|
| 1 | +## Set the API port. Default port numbers are: |
| 2 | +# 7496 - Trader Workstation, real trading |
| 3 | +# 4001 - IB Gateway, real trading |
| 4 | +# 7497 - Trader Workstation, paper trading |
| 5 | +# 4002 - IB Gateway, paper trading |
| 6 | +API_PORT = 7497 |
| 7 | + |
| 8 | +import deephaven_ib as dhib |
| 9 | + |
| 10 | +# Disable read-only mode when connecting to the default ports for paper trading: |
| 11 | +if API_PORT == 7497 or API_PORT == 4002: |
| 12 | + read_only_api = False |
| 13 | +else: |
| 14 | + read_only_api = True |
| 15 | + |
| 16 | +client = dhib.IbSessionTws(host="host.docker.internal", port=API_PORT, read_only=read_only_api) |
| 17 | +client.connect() |
| 18 | + |
| 19 | +if client.is_connected(): |
| 20 | + print('Client connected!') |
| 21 | +else: |
| 22 | + raise RuntimeError("Client not connected!") |
| 23 | + |
| 24 | + |
| 25 | +def check_table_size(dh_table, table_name, expected_size=1): |
| 26 | + table_size = dh_table.size |
| 27 | + if (table_size < expected_size): |
| 28 | + raise RuntimeError( |
| 29 | + 'Table "' + table_name + '" has ' + str(table_size) + ' rows! (Expected ' + str(expected_size) + '.)') |
| 30 | + else: |
| 31 | + print('Found ' + str(table_size) + ' rows in table "' + table_name + '".') |
| 32 | + |
| 33 | + |
| 34 | +# Get the Deephaven table of position updates, and use 'last_by' to find the |
| 35 | +# current positions (i.e. last row for each ContractId): |
| 36 | +positions = client.tables['accounts_positions'].last_by(['ContractId']) |
| 37 | + |
| 38 | +positions.j_table.awaitUpdate() |
| 39 | + |
| 40 | +check_table_size(positions, "pos") |
| 41 | + |
| 42 | +########## |
| 43 | +########## |
| 44 | +########## |
| 45 | + |
| 46 | +import numpy as np |
| 47 | +from deephaven.pandas import to_pandas |
| 48 | + |
| 49 | +# Get a DH table containing only the distinct Symbols: |
| 50 | +pos_syms = positions.select_distinct(['Symbol']) |
| 51 | +mkt_data_syms_set = set(to_pandas(pos_syms)['Symbol'].values) |
| 52 | +print('Found ' + str(len(mkt_data_syms_set)) + ' position symbols: ' + str(mkt_data_syms_set)) |
| 53 | + |
| 54 | +# Add SPY to the set of symbols to request data for: |
| 55 | +mkt_data_syms_set.add('SPY') |
| 56 | + |
| 57 | +from ibapi.contract import Contract |
| 58 | + |
| 59 | +c = Contract() |
| 60 | +c.secType = 'STK' |
| 61 | +c.exchange = 'SMART' |
| 62 | +c.currency = 'USD' |
| 63 | + |
| 64 | +c.symbol = None |
| 65 | +for sym in mkt_data_syms_set: |
| 66 | + print('Requesting data for symbol=' + str(sym)) |
| 67 | + c.symbol = sym |
| 68 | + |
| 69 | + rc = client.get_registered_contract(c) |
| 70 | + client.request_bars_historical( |
| 71 | + rc, |
| 72 | + duration=dhib.Duration.days(253), |
| 73 | + bar_size=dhib.BarSize.DAY_1, |
| 74 | + bar_type=dhib.BarDataType.ADJUSTED_LAST, |
| 75 | + keep_up_to_date=False |
| 76 | + ) |
| 77 | + |
| 78 | +# Retrieve the Deephaven table of historical data bars: |
| 79 | +hist_data_bars = client.tables['bars_historical'] |
| 80 | + |
| 81 | +# Wait for data to be retrieved: |
| 82 | +from time import sleep |
| 83 | + |
| 84 | +sleep(5) |
| 85 | + |
| 86 | +hist_data_bars.j_table.awaitUpdate() |
| 87 | +hist_data_recvd_syms = hist_data_bars.select_distinct(['Symbol']) |
| 88 | +check_table_size(hist_data_recvd_syms, 'hist_data_recvd_syms', len(mkt_data_syms_set)) |
| 89 | + |
| 90 | +########## |
| 91 | +########## |
| 92 | +########## |
| 93 | + |
| 94 | +# Use 'colname_[i-1]' to read a value from the previous row |
| 95 | +hist_data_with_return = hist_data_bars \ |
| 96 | + .update_view(formulas=[ |
| 97 | + 'SameTickerAsPrevRow = Symbol=Symbol_[i-1]', |
| 98 | + 'Last = !SameTickerAsPrevRow ? null : Close_[i-1]', |
| 99 | + 'Chg = Close - Last', |
| 100 | + 'Return = Chg/Last', |
| 101 | +]) |
| 102 | + |
| 103 | +# Join the SPY returns onto the returns for all stocks |
| 104 | +spy = hist_data_with_return.where("Symbol=`SPY`") |
| 105 | +hist_data_with_spy = hist_data_with_return.natural_join(spy, ['Timestamp'], ['SPY_Return=Return']) |
| 106 | + |
| 107 | +########## |
| 108 | +########## |
| 109 | +########## |
| 110 | + |
| 111 | + |
| 112 | +# Install sklearn and run a linear regression to calculate betas |
| 113 | +print("Installing sklearn...") |
| 114 | +import os |
| 115 | + |
| 116 | +os.system("pip install sklearn") |
| 117 | +from sklearn.linear_model import LinearRegression |
| 118 | + |
| 119 | +## Use a DynamicTableWriter to store regression results in a Deephaven table |
| 120 | +import deephaven.dtypes as dht |
| 121 | +from deephaven import DynamicTableWriter |
| 122 | +from deephaven.table import Table |
| 123 | + |
| 124 | +table_writer = DynamicTableWriter( |
| 125 | + {"Symbol": dht.string, |
| 126 | + "Beta": dht.double, |
| 127 | + "Intercept": dht.double, |
| 128 | + "R2": dht.double |
| 129 | + } |
| 130 | +) |
| 131 | +regression_results = table_writer.table |
| 132 | + |
| 133 | +# Partition the table, creating a distinct table for each Symbol: |
| 134 | +data_partitioned = hist_data_with_spy.partition_by(['Symbol']) |
| 135 | + |
| 136 | +print('Calculating betas...') |
| 137 | +for symbol in mkt_data_syms_set: |
| 138 | + print('Calculating beta for ' + symbol + '...') |
| 139 | + returns_for_betas = data_partitioned.get_constituent(symbol) \ |
| 140 | + .where(['!isNull(Return)', '!isNull(SPY_Return)']) |
| 141 | + |
| 142 | + returns_for_betas_df = to_pandas(returns_for_betas) |
| 143 | + |
| 144 | + reg = LinearRegression() |
| 145 | + X = returns_for_betas_df['SPY_Return'].values.reshape(-1, 1) |
| 146 | + Y = returns_for_betas_df['Return'] |
| 147 | + reg.fit(X, Y) |
| 148 | + r2 = reg.score(X, Y).real |
| 149 | + |
| 150 | + print(symbol + ' coef: ' + str(reg.coef_) + |
| 151 | + '; intercept: ' + str(reg.intercept_) + |
| 152 | + '; R2: ', str(r2)) |
| 153 | + |
| 154 | + # Append to the 'regression_results' table: |
| 155 | + table_writer.write_row( |
| 156 | + symbol, |
| 157 | + reg.coef_[0], |
| 158 | + reg.intercept_, |
| 159 | + r2 |
| 160 | + ) |
| 161 | +print('Finished calculating betas!') |
| 162 | + |
| 163 | +########## |
| 164 | +########## |
| 165 | +########## |
| 166 | + |
| 167 | + |
| 168 | +# Request live prices: |
| 169 | +ticks_price = client.tables['ticks_price'] |
| 170 | +live_prices = ticks_price.last_by(['ContractId']) |
| 171 | + |
| 172 | +for sym in mkt_data_syms_set: |
| 173 | + print('Requesting data for symbol=' + str(sym)) |
| 174 | + c.symbol = sym |
| 175 | + rc = client.get_registered_contract(c) |
| 176 | + client.request_market_data( |
| 177 | + rc, |
| 178 | + snapshot=False |
| 179 | + ) |
| 180 | + |
| 181 | +sleep(2) |
| 182 | +live_prices.j_table.awaitUpdate() |
| 183 | +check_table_size(live_prices, 'live_prices', len(mkt_data_syms_set)) |
| 184 | + |
| 185 | +########## |
| 186 | +########## |
| 187 | +########## |
| 188 | + |
| 189 | +# Join the table of betas onto the positions |
| 190 | +pos_with_beta = positions.natural_join(live_prices, ['ContractId'], ['Price']) \ |
| 191 | + .natural_join(regression_results, ['Symbol'], ['Beta', 'R2']) \ |
| 192 | + .view([ |
| 193 | + 'Symbol', |
| 194 | + 'ContractId', |
| 195 | + 'SecType', |
| 196 | + 'Currency', |
| 197 | + 'Position', |
| 198 | + 'PosValue = Position * Price', |
| 199 | + 'Price', |
| 200 | + 'AvgCost', |
| 201 | + 'PNL = PosValue - AvgCost * Position', |
| 202 | + 'Beta', |
| 203 | + 'R2', |
| 204 | + 'SPYBetaValue = Beta * PosValue', |
| 205 | +]) |
| 206 | + |
| 207 | +########## |
| 208 | +########## |
| 209 | +########## |
| 210 | + |
| 211 | +# Calculate hedge, excluding positions with a very low R2: |
| 212 | +hedge_shares = pos_with_beta \ |
| 213 | + .view([ |
| 214 | + 'PosValue', |
| 215 | + 'WeightedBeta = Beta * PosValue', |
| 216 | + 'SPYBetaValue', |
| 217 | + 'SPYBetaValueForHedge = R2 > 1/5 ? SPYBetaValue : 0' |
| 218 | +]) \ |
| 219 | + .sum_by() \ |
| 220 | + .natural_join(live_prices.where('Symbol=`SPY`'), [], ['SPY_Price=Price']) \ |
| 221 | + .view([ |
| 222 | + 'PortfolioValue = PosValue', |
| 223 | + 'PortfolioBeta = WeightedBeta / PosValue', |
| 224 | + 'SPYBetaValue', |
| 225 | + 'SPYBetaValueForHedge', |
| 226 | + 'HedgeShares = -round(SPYBetaValueForHedge / SPY_Price)', |
| 227 | + 'HedgeCost = HedgeShares * SPY_Price', |
| 228 | + 'SPY_Price' |
| 229 | +]) |
| 230 | + |
| 231 | +########## |
| 232 | +########## |
| 233 | +########## |
| 234 | + |
| 235 | +# Set send_hedge_order to True to submit the order, not just generate it. |
| 236 | +# (Must also set read_only to False when creating the IbSessionTws instance.) |
| 237 | +send_hedge_order = False |
| 238 | + |
| 239 | +from ibapi.order import Order |
| 240 | + |
| 241 | +c.symbol = "SPY" |
| 242 | +rc = client.get_registered_contract(c) |
| 243 | +print(c) |
| 244 | + |
| 245 | +# Extract the hedge information from the hedge_shares table: |
| 246 | +hedge_info = hedge_shares.j_table.getRecord(0, 'HedgeShares', 'SPY_Price') |
| 247 | +hedge_qty = hedge_info[0] |
| 248 | +hedge_last_px = hedge_info[1] |
| 249 | +hedge_side = "BUY" if hedge_qty > 0 else "SELL" |
| 250 | +hedge_limit_px = hedge_last_px + 0.05 * (1 if hedge_side is "BUY" else -1) |
| 251 | + |
| 252 | +# Create an order with the IB API: |
| 253 | +order = Order() |
| 254 | +# order.account = "<account number>" |
| 255 | +order.action = hedge_side |
| 256 | +order.orderType = "LIMIT" |
| 257 | +order.totalQuantity = hedge_qty |
| 258 | +order.lmtPrice = hedge_limit_px |
| 259 | +order.eTradeOnly = False |
| 260 | +order.firmQuoteOnly = False |
| 261 | + |
| 262 | +print('Order: ' + str(order)) |
| 263 | + |
| 264 | +if send_hedge_order: |
| 265 | + print('***** Sending order to ' + order.action + ' ' + str( |
| 266 | + order.totalQuantity) + ' shares of ' + c.symbol + '! *****') |
| 267 | + req = client.order_place(rc, order) |
| 268 | + |
| 269 | +else: |
| 270 | + print('Not actually sending order.') |
| 271 | + |
| 272 | +# To cancel orders: |
| 273 | +# req.cancel() |
| 274 | +# client.order_cancel_all() |
0 commit comments