Skip to main content

Fully automated trading with Deephaven and Interactive Brokers

· 7 min read
Stable Diffusion prompt: new york stock exchange trading floor, robot standing in middle, cozy vibe, realistic, intricate, Seed-3816647 Steps-40 Guidance-7.5
Chip Kent
Use Deephaven and IB to build your own fully automated market maker.

Many of the world's largest and most profitable hedge funds, such as Renaissance Technologies, Virtu, Citadel, Two Sigma, Tower Research, and Jump Trading, use algorithms to determine when to buy and sell stocks, futures, bonds, options, and other securities. They have replaced human traders with emotionless computers that can act at speeds far beyond human cognition.

If you have a brilliant algorithmic trading idea, you no longer need to join a top hedge fund to trade it.

Using Deephaven and Interactive Brokers, a very popular brokerage in the quantitative finance world, you can build your own fully-automated trading strategy. All you need is deephaven-ib.

To illustrate the process, let's write a market maker.

"Market maker" may be an unfamiliar term, but the concept is straightforward. A market maker is a strategy that quotes both a buy and a sell price for a security, hoping to make a profit by buying at the low price and selling at the high price. Market makers typically turn over positions quickly, hoping to make a small amount of profit on each trade.

img

To learn how we put this dashboard and automated market making system together, read on.

Getting started

To get started, you will need to follow the directions on the deephaven-ib GitHub page to launch deephaven-ib.

Next, we need to:

Click to see the code
from ibapi.contract import Contract
from ibapi.order import Order

import deephaven_ib as dhib
from deephaven.updateby import ema_time
from deephaven import time_table
from deephaven.plot import Figure
from deephaven.plot.selectable_dataset import one_click
from deephaven.plot import PlotStyle


###########################################################################
# WARNING: THIS SCRIPT EXECUTES TRADES!! ONLY USE ON PAPER TRADING ACCOUNTS
###########################################################################

print("==============================================================================================================")
print("==== Create a client and connect.")
print("==== ** Accept the connection in TWS **")
print("==============================================================================================================")

client = dhib.IbSessionTws(host="host.docker.internal", port=7497, client_id=0, download_short_rates=False, read_only=False)
print(f"IsConnected: {client.is_connected()}")

client.connect()
print(f"IsConnected: {client.is_connected()}")

## Setup

account = "DU4943848"
max_position_dollars = 10000.0
ema_t = "00:02:00"

Get IB real-time tables

Once that is done, it is time to get real-time input data tables from Interactive Brokers. Our market maker will need market prices, positions, and orders. All of this data is available in the deephaven-ib client.

errors = client.tables["errors"]
requests = client.tables["requests"]
positions = client.tables["accounts_positions"].where("Account = account")
ticks_bid_ask = client.tables["ticks_bid_ask"]
orders_submitted = client.tables["orders_submitted"].where("Account = account")
orders_status = client.tables["orders_status"]
orders_exec_details = client.tables["orders_exec_details"].where("Account = account")

Subscribe to market data

Next, it is time to subscribe to data for the securities we want to make markets in. In this case, we are requesting data for GOOG, BAC, and for AAPL. All three subscribe to market data from the Interactive Brokers Smart router. GOOG and BAC will place orders using the Interactive Brokers Smart router, and AAPL will place orders on the New York Stock Exchange (NYSE).

Click to see the code
print("==============================================================================================================")
print("==== Request data.")
print("==============================================================================================================")

registered_contracts_data = {}
registred_contracts_orders = {}

def add_contract(symbol: str, exchange: str="SMART") -> None:
"""
Configure a contract for trading.
:param symbol: Symbol to trade.
:param exchange: exchange where orders get routed.
:return: None
"""

contract = Contract()
contract.symbol = symbol
contract.secType = "STK"
contract.currency = "USD"
contract.exchange = "SMART"

rc = client.get_registered_contract(contract)
id = rc.contract_details[0].contract.conId
registered_contracts_data[id] = rc
client.request_tick_data_realtime(rc, dhib.TickDataType.BID_ASK)
print(f"Registered contract: id={id} rc={rc}")

if exchange != "SMART":
contract.exchange = "NYSE"
rc = client.get_registered_contract(contract)

registred_contracts_orders[id] = rc
print(f"Registered contract: id={id} rc={rc}")


add_contract("GOOG")
add_contract("BAC")
add_contract("AAPL", exchange="NYSE")

Make predictions

Now that we have data, it is time to make some predictions. Our model is very simple. We use a 2 minute moving average to compute our predicted price as well as one standard deviation error bars (Bollinger Bands). We will place buy orders at the low price and sell orders at the high price.

Click to see the code
print("==============================================================================================================")
print("==== Compute predictions.")
print("==============================================================================================================")

preds = ticks_bid_ask \
.update_view(["MidPrice=0.5*(BidPrice+AskPrice)", "MidPrice2=MidPrice*MidPrice"]) \
.update_by([ema_time("Timestamp", ema_t, ["PredPrice=MidPrice","MidPrice2Bar=MidPrice2"])], by="Symbol") \
.view([
"ReceiveTime",
"Timestamp",
"ContractId",
"Symbol",
"BidPrice",
"AskPrice",
"MidPrice",
"PredPrice",
"PredSD = sqrt(MidPrice2Bar-PredPrice*PredPrice)",
"PredLow=PredPrice-PredSD",
"PredHigh=PredPrice+PredSD",
])

preds_start = preds.first_by("Symbol").view(["Symbol", "Timestamp"])
preds = preds.natural_join(preds_start, on="Symbol", joins="TimestampFirst=Timestamp")

preds_one_click = one_click(preds, by=["Symbol"], require_all_filters=True)

preds_plot = Figure() \
.plot_xy("BidPrice", t=preds_one_click, x="Timestamp", y="BidPrice") \
.plot_xy("AskPrice", t=preds_one_click, x="Timestamp", y="AskPrice") \
.plot_xy("MidPrice", t=preds_one_click, x="Timestamp", y="MidPrice") \
.plot_xy("PredPrice", t=preds_one_click, x="Timestamp", y="PredPrice") \
.plot_xy("PredLow", t=preds_one_click, x="Timestamp", y="PredLow") \
.plot_xy("PredHigh", t=preds_one_click, x="Timestamp", y="PredHigh") \
.show()

Automatically manage orders

Now we come to the cool part -- automatically generating and canceling orders. To do this, we create a Python function, update_orders, that uses the deephaven-ib API to cancel existing orders on a security and then place new orders at new prices. Because native Python functions can be used in Deephaven queries, update can be used to call update_orders every time the prediction for a security updates.

In our case, canceling and replacing orders every time the market ticks is overkill, so we use snapshot to downsample the predictions once a minute.

Click to see the code
print("==============================================================================================================")
print("==== Generate orders.")
print("==============================================================================================================")

open_orders = {}

def update_orders(contract_id: int, pred_low: float, pred_high: float, buy_order: bool, sell_order:bool) -> int:
"""
Update orders on a contract. First existing orders are canceled. Then new buy/sell limit orders are placed.
:param contract_id: Contract id.
:param pred_low: Price for buy limit orders.
:param pred_high: Price for sell limit orders.
:param buy_order: True to post a buy order; False to not post a buy order.
:param sell_order: True to post a sell order; False to not post a sell order.
:return: Number of orders submitted.
"""

if contract_id in open_orders:
for order in open_orders[contract_id]:
# print(f"Canceling order: contract_id={contract_id} order_id={order.request_id}")
order.cancel()

new_orders = []
rc = registred_contracts_orders[contract_id]

if sell_order:
order_sell = Order()
order_sell.account = account
order_sell.action = "SELL"
order_sell.orderType = "LIMIT"
order_sell.totalQuantity = 100
order_sell.lmtPrice = round( pred_high, 2)
order_sell.transmit = True

order = client.order_place(rc, order_sell)
new_orders.append(order)

if buy_order:
order_buy = Order()
order_buy.account = account
order_buy.action = "BUY"
order_buy.orderType = "LIMIT"
order_buy.totalQuantity = 100
order_buy.lmtPrice = round( pred_low, 2)
order_buy.transmit = True

order = client.order_place(rc, order_buy)
new_orders.append(order)

open_orders[contract_id] = new_orders
return len(new_orders)


orders = time_table("00:01:00") \
.rename_columns("SnapTime=Timestamp") \
.snapshot(preds.last_by(["Symbol"])) \
.where(f"Timestamp > TimestampFirst + '{ema_t}'") \
.natural_join(positions, on="ContractId", joins="Position") \
.update_view([
"Position = replaceIfNull(Position, 0.0)",
"PositionDollars = Position * MidPrice",
"MaxPositionDollars = max_position_dollars",
"BuyOrder = PositionDollars < MaxPositionDollars",
"SellOrder = PositionDollars > -MaxPositionDollars",
]) \
.update("NumNewOrders = (long)update_orders(ContractId, PredLow, PredHigh, BuyOrder, SellOrder)")

Analyze trades

Now, it would be very useful to understand how well the algorithm is working. To do this, let's plot the trades on the same axes as the predictions.

Click to see the code
print("==============================================================================================================")
print("==== Plot trade executions.")
print("==============================================================================================================")


trades = orders_exec_details \
.natural_join(preds_start, on="Symbol", joins="TimestampFirst=Timestamp") \
.where("ReceiveTime >= TimestampFirst") \
.view(["Timestamp=ReceiveTime", "ContractId", "Symbol", "ExecutionExchange", "Side", "Shares", "Price"])

buys_one_click = one_click(trades.where("Side=`BOT`"), by=["Symbol"], require_all_filters=True)
sells_one_click = one_click(trades.where("Side=`SLD`"), by=["Symbol"], require_all_filters=True)

execution_plot = Figure() \
.plot_xy("BidPrice", t=preds_one_click, x="Timestamp", y="BidPrice") \
.plot_xy("AskPrice", t=preds_one_click, x="Timestamp", y="AskPrice") \
.plot_xy("MidPrice", t=preds_one_click, x="Timestamp", y="MidPrice") \
.plot_xy("PredPrice", t=preds_one_click, x="Timestamp", y="PredPrice") \
.plot_xy("PredLow", t=preds_one_click, x="Timestamp", y="PredLow") \
.plot_xy("PredHigh", t=preds_one_click, x="Timestamp", y="PredHigh") \
.twin() \
.axes(plot_style=PlotStyle.SCATTER) \
.plot_xy("Buys", t=buys_one_click, x="Timestamp", y="Price") \
.plot_xy("Sells", t=sells_one_click, x="Timestamp", y="Price") \
.show()

You have now created your own fully-automated real-time market maker! It couldn't be easier.

Customize

What if you want to start market making AMZN? That's easy. You just need to run one line of code.

add_contract("AMZN")

These same concepts can be used to develop algorithmic trading strategies that execute on Alpaca, BiNance, CoinBase, or any other exchange or broker that offers an API.

What algorithmic trading will you do with Deephaven? Tell us on Slack.