0%

【How 2】 Set Up Trading API Template In Python - Placing orders with Interactive Brokers

This is the second part of the Set Up Trading API Template In Python. We’re going to focus on implementing the rest of the functions in our Interactive Broker class.


If you enjoy reading this and my other articles, feel free to join Medium membership program to read more about Quantitative Trading Strategy.


Previous researches

Recap and what’s the next

In the previous post, we get to know how our trading script sends API calls to the IBKR API service via the IB gateway. Also, we have learned how to configure the IB gateway application. Lastly, we also showcase the code snippet to get the available cash balance and the total investment market value under your account from the server. Now, we’ll look at the rest of the functions in our InteractiveBrokerTradeAPI class.

  • Get a much more detailed status report with get_account_detail
  • Fetch the market calendar
  • How to create a valid order for Interactive Broker

API document reference

ib_insync

Get a deep dive status report with get_account_detail

The get_account_detail() in our earlier example has successfully extracted the TotalCashBalance and StockMarketValue from the ib.accountValues() response. Yet, if we would like to know more about our portfolio status, we can include two more calls to obtain more information about our portfolio: 1. positions in our portfolio, 2. the orders that we placed in the past 24 hours.

For 1., we use ib.portfolio() to acquire information on the stocks we hold. We will extract the position size and the market value of each symbol, thus gaining a bigger picture of how our portfolio looks like.

1
2
[PortfolioItem(contract=Stock(conId=42454579, symbol='SHV', right='0', primaryExchange='NASDAQ', currency='USD', localSymbol='SHV', tradingClass='NMS'), position=427.0, marketPrice=109.9701004, marketValue=46957.23, averageCost=109.98463535, unrealizedPNL=-6.21, realizedPNL=0.0, account='DU4399668'),
PortfolioItem(contract=Stock(conId=39622943, symbol='SSO', right='0', primaryExchange='ARCA', currency='USD', localSymbol='SSO', tradingClass='SSO'), position=1060.0, marketPrice=47.5340004, marketValue=50386.04, averageCost=48.7388397, unrealizedPNL=-1277.13, realizedPNL=-62.91, account='DU4399668')]

Response from ib.portfolio() call

As for 2., we use ib.trades() to obtain the trades we made in the past 24 hours. Remember, the Interactive broker holds this information for only 24 hours or so, and you won’t be able to retrieve this piece once the server drops this information. Therefore, we will find a way to address this in another post to persist the order-related information. In the below Trade objects, we extract the information we need such as order id, average price, order status, commission cost, and so on for each symbol.

1
[Trade(contract=Stock(conId=42454579, symbol='SHV', right='?', exchange='SMART', currency='USD', localSymbol='SHV', tradingClass='NMS'), order=Order(permId=423185966, action='SELL', orderType='MKT', lmtPrice=0.0, auxPrice=0.0, tif='DAY', ocaType=3, displaySize=2147483647, rule80A='0', openClose='', volatilityType=0, deltaNeutralOrderType='None', referencePriceType=0, account='DU4399668', clearingIntent='IB', cashQty=0.0, dontUseAutoPriceForHedge=True, filledQuantity=1.0, refFuturesConId=2147483647, shareholder='Not an insider or substantial shareholder'), orderStatus=OrderStatus(orderId=0, status='Filled', filled=0.0, remaining=0.0, avgFillPrice=0.0, permId=0, parentId=0, lastFillPrice=0.0, clientId=0, whyHeld='', mktCapPrice=0.0), fills=[Fill(contract=Stock(conId=42454579, symbol='SHV', right='?', exchange='SMART', currency='USD', localSymbol='SHV', tradingClass='NMS'), execution=Execution(execId='00025b49.63971bea.01.01', time=datetime.datetime(2022, 12, 12, 17, 2, 38, tzinfo=datetime.timezone.utc), acctNumber='DU4399668', exchange='EDGEA', side='SLD', shares=1.0, price=109.97, permId=423185966, clientId=0, orderId=0, liquidation=0, cumQty=1.0, avgPrice=109.97, orderRef='', evRule='', evMultiplier=0.0, modelCode='', lastLiquidity=2), commissionReport=CommissionReport(execId='00025b49.63971bea.01.01', commission=1.002648, currency='USD', realizedPNL=-1.017284, yield_=0.0, yieldRedemptionDate=0), time=datetime.datetime(2022, 12, 12, 17, 2, 38, tzinfo=datetime.timezone.utc))], log=[TradeLogEntry(time=datetime.datetime(2022, 12, 12, 17, 2, 38, tzinfo=datetime.timezone.utc), status='Filled', message='Fill 1.0@109.97', errorCode=0)], advancedError='')]

Response from ib.trades() call

Combining everything we talked about above, we can construct our get_account_detail() function as below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def get_account_detail(self):
self.accounts = self.client.managedAccounts()

acc_data = []
for account in self.accounts:
acc = {}
acc['account'] = account
data = self.client.accountValues(account)
acc['cash'] = 0
acc['total_assets'] = 0
for row in data:
if row.tag in ['TotalCashBalance'] and row.currency == self.currency:
acc['cash'] = row.value
acc['total_assets'] += float(row.value)
elif row.tag in ['StockMarketValue'] and row.currency == self.currency:
acc['total_assets'] += float(row.value)
acc_data.append(acc)

pos_data = []
data = self.client.portfolio()
for position in data:
pos = {}

pos['code'] = position.contract.symbol
pos['qty'] = position.position
pos['cost_price'] = position.averageCost
pos['market_val'] = position.marketValue
pos['pl_val'] = position.unrealizedPNL
if pos['cost_price'] * pos['qty'] == 0:
pos['pl_ratio'] = 0
else:
pos['pl_ratio'] = pos['pl_val'] / (pos['cost_price'] * pos['qty'])
pos_data.append(pos)

orders_data = []
data = self.client.trades()
for order in data:
o = {}
o['order_id'] = order.order.orderId
o['order_status'] = order.orderStatus.status
o['create_time'] = order.log[-1].time
o['trd_side'] = order.order.action
o['order_type'] = order.order.action
o['code'] = order.contract.symbol
orders_data.append(o)
return acc_data, pos_data, orders_data

Full code of get_account_detail() function

Fetch the market calendar

The trading hours information in ib_insync package is quite discreet. After reading the API document very carefully, I finally found it in the response of ib.reqContractDetails() call and look like this:

1
2
3
4
5
6
7
8
9
10
11
12
ContractDetails(contract=Contract(secType='STK', conId=756733, symbol='SPY', exchange='SMART', primaryExchange='ARCA', currency='USD', localSymbol='SPY',
tradingClass='SPY'), marketName='SPY', minTick=0.01, orderTypes='ACTIVETIM,AD,ADJUST,ALERT,ALGO,ALLOC,AON,AVGCOST,BASKET,BENCHPX,CASHQTY,COND,CONDORDER,DARKONLY,
DARKPOLL,DAY,DEACT,DEACTDIS,DEACTEOD,DIS,DUR,GAT,GTC,GTD,GTT,HID,IBKRATS,ICE,IOC,LIT,LMT,LOC,MIDPX,MIT,MKT,MOC,MTL,NGCOMB,NODARK,NONALGO,OCA,OPG,OPGREROUT,PEGBENCH,
PEGMID,POSTATS,POSTONLY,PREOPGRTH,PRICECHK,REL,REL2MID,RELPCTOFS,RTH,SCALE,SCALEODD,SCALERST,SIZECHK,SMARTSTG,SNAPMID,SNAPMKT,SNAPREL,STP,STPLMT,SWEEP,TRAIL,TRAILLIT,
TRAILLMT,TRAILMIT,WHATIF', validExchanges='SMART,AMEX,NYSE,CBOE,PHLX,ISE,CHX,ARCA,ISLAND,DRCTEDGE,BEX,BATS,EDGEA,CSFBALGO,JEFFALGO,BYX,IEX,EDGX,FOXRIVER,PEARL,NYSENAT,
LTSE,MEMX,IBEOS,PSX', priceMagnifier=1, underConId=0, longName='SPDR S&P 500 ETF TRUST', contractMonth='', industry='', category='', subcategory='', timeZoneId='US/Eastern',
tradingHours='20221212:0400-20221212:2000;20221213:0400-20221213:2000;20221214:0400-20221214:2000;20221215:0400-20221215:2000;20221216:0400-20221216:2000',
liquidHours='20221212:0930-20221212:1600;20221213:0930-20221213:1600;20221214:0930-20221214:1600;20221215:0930-20221215:1600;20221216:0930-20221216:1600', evRule='',
evMultiplier=0, mdSizeMultiplier=1, aggGroup=1, underSymbol='', underSecType='', marketRuleIds='26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26',
secIdList=[TagValue(tag='ISIN', value='US78462F1030')], realExpirationDate='', lastTradeTime='', stockType='ETF', minSize=0.0001, sizeIncrement=0.0001,
suggestedSizeIncrement=100.0, cusip='', ratings='', descAppend='', bondType='', couponType='', callable=False, putable=False, coupon=0, convertible=False,
maturity='', issueDate='', nextOptionDate='', nextOptionType='', nextOptionPartial=False, notes='')

Response of ib.reqContractDetails() functions

The trading calendar resides in this response, and we can extract them by parsing them like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def is_market_open(self, offset_days=0):
spy_contract = ib_insync.Stock('SPY', 'SMART', self.currency)
self.client.qualifyContracts(spy_contract)
trading_days = self.client.reqContractDetails(spy_contract)[0].liquidHours
trading_days_dict = {d.split(':')[0]:d.split(':')[1] for d in trading_days.split(';')}
today_str = (datetime.now().astimezone(self.timezone) + timedelta(days=offset_days)).strftime('%Y%m%d')

for k, v in trading_days_dict.items():
if (today_str in k) and (v == 'CLOSED'):
return False

return True

def is_market_open_now(self):
spy_contract = ib_insync.Stock('SPY', 'SMART', self.currency)
self.client.qualifyContracts(spy_contract)
trading_days = self.client.reqContractDetails(spy_contract)[0].liquidHours
trading_days_list = [d.split('-') for d in trading_days.split(';')]

day_str = datetime.now().astimezone(self.timezone).strftime('%Y%m%d')
time_str = datetime.now().astimezone(self.timezone).strftime('%H%M')

for d in trading_days_list:
if len(d) > 1 and day_str in d[0].split()[0]:
if time_str > d[0].split(':')[1] and time_str < d[1].split(':')[1]:
return True

return False

Full code of is_market_open() and is_market_open_now() functions

How to create a valid order for Interactive Broker

In order to create a valid order that Interactive Broker could recognize, there are a few steps to follow:

  1. Specify the symbol and the currency used. Use contract = ib_insync.Stock(symbol, 'SMART', self.currency) to create a contract object for later use. Stock symbol would be the first parameter, the name of the stock exchange be the second, and the currency symbol (here we use USD) would be the third.
  2. Make a query to the broker to filter and find the related stock information. ib.qualifyContracts(contract) would activate the contract object and infuse live data from the stock exchange.

    Contract object before and after using ib.qualifyContracts() to infuse correct data

  3. We need the latest quote price in order to calculate how many shares we would like to purchase. First of all, you need to specify the reqMarketDataType to tell the server which type of data you’re requesting. There are four market data types:
    1. 1 - Live market data: (top of the book)
    2. 2 - Frozen data (at the close)
    3. 3 - Delayed data (can be used if there are no live subscriptions)
    4. 4 - Frozen Delayed data (outside of regular trading hours)
      Once we have specified the market data type, we’re all set to request the quote price from the server using the contract in your first parameter and snapshot to True.
      1
      2
      3
      4
      5
      6
      7
      8
      ib.reqMarketDataType(3)
      quote = ib.reqMktData(
      contract,
      genericTickList="",
      snapshot=True,
      regulatorySnapshot=False,
      mktDataOptions=None
      )
      One thing worth mentioning is that, do you remember the reason why I’m using the ib_insync package instead of the native IBKR API? Instead of saying fetching a quote price from the API server, subscribing to the periodical price change would be a better way to put it. We first subscribe to the price bar to get 5-minute, 10-minute, or one-day price data, and another thread would be created to stream the price data. Therefore, to extract the quote price from the returned object, you first must ensure the quote price has been successfully returned/received.

      Notes: Before requesting a market quote, you need to subscribe to the market data on the IBKR platform. You can find the management page in the TWS or IB gateway tab “Account” -> “Manage Account” -> “Subscribe Market Data/Research”

  4. Lastly, other than using sleep() function call to ensure that we have received the price data from the server, we can also assign the callback function to monitor the status of a specific order status change. Here we use a global configuration under the ib instance to specify this callback function by using ib.orderStatusEvent += [callback function]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
@contextmanager
def connect(self):
self.client = ib_insync.IB()
# Newly added
self.client.orderStatusEvent += self.__order_status
self.client.connect(
IB_TWS_URI,
# IB_GATEWAY_PAPER_PORT,
IB_TWS_PAPER_PORT,
IB_TWS_CLIENT_ID
)

yield self

self.client.disconnect()
self.client.sleep(2)


def place_order(self, symbol: str, quantity: int, price: float=0):
contract = ib_insync.Stock(symbol.upper(), 'SMART', self.currency)
self.client.qualifyContracts(contract)
if quantity >= 0.0:
order = ib_insync.MarketOrder('BUY', quantity)
else:
order = ib_insync.MarketOrder('SELL', -quantity)
trade = self.client.placeOrder(contract, order)
self.client.sleep(5)

return True

def get_last_price_from_quote(self):
contract = ib_insync.Stock(symbol.upper(), 'SMART', self.currency)
self.client.qualifyContracts(contract)
# ib.reqMarketDataType(1) # Live market data: (top of the book)
# ib.reqMarketDataType(2) # Frozen data (at the close)
# ib.reqMarketDataType(3) # Delayed data (can be used if there is no live subscriptions)
# ib.reqMarketDataType(4) # Frozen Delayed data (outside of regular trading hours)
self.client.reqMarketDataType(3)
quote = self.client.reqMktData(
contract,
genericTickList="",
snapshot=True,
regulatorySnapshot=False,
mktDataOptions=None
)

for _ in range(10):
if math.isnan(quote.last):
self.client.sleep(1)
else:
return quote.last
logger.logger.error('{}[{}]: {}'.format(sys._getframe().f_code.co_name, symbol, 'No last price in quote'))
self.notifier.send_msg('{}[{}]'.format(sys._getframe().f_code.co_name, symbol), 'No last price in quote')
return 0


def __order_status(self, trade):
'''
Call back function for checking order status
'''
print(f'Order [{trade.contract.symbol}] status updated: {trade.orderStatus.status}')
match trade.orderStatus.status:
case 'Filled':
print(f'{trade=}')
self.update_order_from_filledEvent_in_db(trade)
case 'PendingSubmit':
# print(f'Pending submit: {trade}')
pass
case 'Submitted':
# print(f'Submitted: {trade}')
pass
case _:
# print(f'Others: {trade.orderStatus.status}')
pass

Let’s wrap it up

We have all the pieces ready except the get_transaction(), which we will talk about it in another post as we need to take Database management into account. Let’s now add some test code so that you can also place the order.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# test.py
from contextlib import contextmanager
import ib_insync
from modules.broker.TradeAPI import AbstractTradeInterface
from datetime import datetime, timedelta
import math
from zoneinfo import ZoneInfo

class InteractiveBrokerTradeAPI(AbstractTradeInterface):
def __init__(self,currency='USD'):
self.client = None
self.accounts = []
self.currency = currency
self.timezone = ZoneInfo('US/Eastern')

@contextmanager
def connect(self):
self.client = ib_insync.IB()
# Newly added
self.client.orderStatusEvent += self.__order_status
self.client.connect('127.0.0.1', 7497, 101)
print("="*30)
print("Connection established")
print("="*30)

yield self

self.client.disconnect()
self.client.sleep(2)
print("="*30)
print("Connection closed")
print("="*30)

def get_account_detail(self):
self.accounts = self.client.managedAccounts()

acc_data = []
for account in self.accounts:
acc = {}
acc['account'] = account
data = self.client.accountValues(account)
acc['cash'] = 0
acc['total_assets'] = 0
for row in data:
if row.tag in ['TotalCashBalance'] and row.currency == self.currency:
acc['cash'] = row.value
acc['total_assets'] += float(row.value)
elif row.tag in ['StockMarketValue'] and row.currency == self.currency:
acc['total_assets'] += float(row.value)
acc_data.append(acc)

pos_data = []
data = self.client.portfolio()
for position in data:
pos = {}

pos['code'] = position.contract.symbol
pos['qty'] = position.position
pos['cost_price'] = position.averageCost
pos['market_val'] = position.marketValue
pos['pl_val'] = position.unrealizedPNL
if pos['cost_price'] * pos['qty'] == 0:
pos['pl_ratio'] = 0
else:
pos['pl_ratio'] = pos['pl_val'] / (pos['cost_price'] * pos['qty'])
pos_data.append(pos)

orders_data = []
data = self.client.trades()
for order in data:
o = {}
o['order_id'] = order.order.permId
o['order_status'] = order.orderStatus.status
o['create_time'] = order.log[-1].time
o['trd_side'] = order.order.action
o['order_type'] = order.order.action
o['code'] = order.contract.symbol
orders_data.append(o)
return acc_data, pos_data, orders_data

def place_order(self, symbol: str, quantity: int, price: float=0):
contract = ib_insync.Stock(symbol.upper(), 'SMART', self.currency)
self.client.qualifyContracts(contract)
if quantity >= 0.0:
order = ib_insync.MarketOrder('BUY', quantity)
else:
order = ib_insync.MarketOrder('SELL', -quantity)
trade = self.client.placeOrder(contract, order)
self.client.sleep(5)

return True

def get_last_price_from_quote(self, symbol:str):
contract = ib_insync.Stock(symbol.upper(), 'SMART', self.currency)
# self.client.reqMarketDataType(3)
self.client.reqMarketDataType(3)
self.client.qualifyContracts(contract)
quote = self.client.reqMktData(
contract,
genericTickList="",
snapshot=True,
regulatorySnapshot=False,
mktDataOptions=None
)

for _ in range(10):
if math.isnan(quote.last):
self.client.sleep(1)
else:
return quote.last
print(f'No last price in quote for {symbol}')
return 0

def __order_status(self, trade):
'''
Call back function for checking order status
'''
print(f'Order [{trade.contract.symbol}] status updated: {trade.orderStatus.status}')
match trade.orderStatus.status:
case 'Filled':
print(f'Order {trade.contract.symbol}, filled.')
case _:
print(f'Others order status: {trade.orderStatus.status}')

def is_market_open(self, offset_days=0):
spy_contract = ib_insync.Stock('SPY', 'SMART', self.currency)
self.client.qualifyContracts(spy_contract)
trading_days = self.client.reqContractDetails(spy_contract)[0].liquidHours
trading_days_dict = {d.split(':')[0]:d.split(':')[1] for d in trading_days.split(';')}
today_str = (datetime.now().astimezone(self.timezone) + timedelta(days=offset_days)).strftime('%Y%m%d')

for k, v in trading_days_dict.items():
if (today_str in k) and (v == 'CLOSED'):
return False

return True

def is_market_open_now(self):
spy_contract = ib_insync.Stock('SPY', 'SMART', self.currency)
self.client.qualifyContracts(spy_contract)
trading_days = self.client.reqContractDetails(spy_contract)[0].liquidHours
trading_days_list = [d.split('-') for d in trading_days.split(';')]

day_str = datetime.now().astimezone(self.timezone).strftime('%Y%m%d')
time_str = datetime.now().astimezone(self.timezone).strftime('%H%M')

for d in trading_days_list:
if len(d) > 1 and day_str in d[0].split()[0]:
if time_str > d[0].split(':')[1] and time_str < d[1].split(':')[1]:
return True

return False

def get_transactions(self):
pass

# Main function
if __name__ == '__main__':
broker = InteractiveBrokerTradeAPI()
print(datetime.now().strftime('Now is %Y-%m-%d'))
with broker.connect() as c:
accounts, positions, orders = c.get_account_detail()
print(ib_insync.util.df(accounts))
print(ib_insync.util.df(positions))
print(ib_insync.util.df(orders))
print("="*30)
market_open = c.is_market_open()
market_open_now = c.is_market_open_now()
print(f'{market_open=}')
print(f'{market_open_now=}')
print("="*30)
print(c.get_last_price_from_quote('SSO'))
if market_open and market_open_now:
last = c.get_last_price_from_quote('AAPL')
print(f'{last=}')
c.place_order('AAPL', 1)

And here’s the output.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Output
Now is 2022-12-14
==============================
Connection established
==============================
account cash total_assets
0 DU4399668 2118.598 99946.918
code qty cost_price market_val pl_val pl_ratio
0 SHV 427.0 109.987048 46970.03 5.56 0.000118
1 SSO 1019.0 48.717576 50858.29 1215.08 0.024476
None
==============================
market_open=True
market_open_now=True
==============================
49.94
last=147.86
Order [AAPL] status updated: PreSubmitted
Others order status: PreSubmitted
Order [AAPL] status updated: Filled
Order AAPL, filled.
==============================
Connection closed
==============================

Voila! Now as long as we schedule the time for each function to run, we will have our automated trading script ready to run! It’s time for you to put on your creative hat and start improvising, adding your own magic to your trading script. See you next time.

Disclaimer: Nothing herein is financial advice or even a recommendation to trade real money. Many platforms exist for simulated trading (paper trading) which can be used for building and developing the strategies discussed. Please use common sense and consult a professional before trading or investing your hard-earned money.

Enjoy reading? Some donations would motivate me to produce more quality content