diff --git a/kinetick/broker.py b/kinetick/broker.py index adced99..075e638 100644 --- a/kinetick/broker.py +++ b/kinetick/broker.py @@ -294,139 +294,11 @@ def _callback(self, caller, msg, **kwargs): symbol, orderId, quantity, filled=True) self._expire_pending_order(symbol, orderId) self._cancel_orphan_orders(orderId) - self._register_trade(order) # filled time.sleep(0.005) self.on_fill(self.get_instrument(order['symbol']), order) - # --------------------------------------- - def _register_trade(self, order): - """ constructs trade info from order data """ - if order['id'] in self.orders.recent: - orderId = order['id'] - else: - orderId = order['parentId'] - # entry / exit? - symbol = order["symbol"] - order_data = self.orders.recent[orderId] - position = self.get_positions(symbol)['position'] - - if position != 0: - # entry - order_data['action'] = "ENTRY" - order_data['position'] = position - order_data['entry_time'] = datetime_to_timezone( - order['time']) - order_data['exit_time'] = None - order_data['entry_order'] = order_data['order_type'] - order_data['entry_price'] = order['avgFillPrice'] - order_data['exit_price'] = 0 - order_data['exit_reason'] = None - - else: - order_data['action'] = "EXIT" - order_data['position'] = 0 - order_data['exit_time'] = datetime_to_timezone(order['time']) - order_data['exit_price'] = order['avgFillPrice'] - - # target / stop? - if order['id'] == order_data['targetOrderId']: - order_data['exit_reason'] = "TARGET" - elif order['id'] == order_data['stopOrderId']: - order_data['exit_reason'] = "STOP" - else: - order_data['exit_reason'] = "SIGNAL" - - # remove from collection - del self.orders.recent[orderId] - - if order_data is None: - return None - - # trade identifier - tradeId = self.strategy.upper() + '_' + symbol.upper() - tradeId = hashlib.sha1(tradeId.encode()).hexdigest() - - # existing trade? - if tradeId not in self.active_trades: - self.active_trades[tradeId] = { - "strategy": self.strategy, - "action": order_data['action'], - "quantity": abs(order_data['position']), - "position": order_data['position'], - "symbol": order_data["symbol"].split('_')[0], - "direction": order_data['direction'], - "entry_time": None, - "exit_time": None, - "duration": "0s", - "exit_reason": order_data['exit_reason'], - "order_type": order_data['order_type'], - "market_price": order_data['price'], - "target": order_data['target'], - "stop": order_data['initial_stop'], - "entry_price": 0, - "exit_price": order_data['exit_price'], - "realized_pnl": 0 - } - if "entry_time" in order_data: - self.active_trades[tradeId]["entry_time"] = order_data['entry_time'] - if "entry_price" in order_data: - self.active_trades[tradeId]["entry_price"] = order_data['entry_price'] - else: - # self.active_trades[tradeId]['direction'] = order_data['direction'] - self.active_trades[tradeId]['action'] = order_data['action'] - self.active_trades[tradeId]['position'] = order_data['position'] - self.active_trades[tradeId]['exit_price'] = order_data['exit_price'] - self.active_trades[tradeId]['exit_reason'] = order_data['exit_reason'] - self.active_trades[tradeId]['exit_time'] = order_data['exit_time'] - - # calculate trade duration - try: - delta = int((self.active_trades[tradeId]['exit_time'] - - self.active_trades[tradeId]['entry_time']).total_seconds()) - days, remainder = divmod(delta, 86400) - hours, remainder = divmod(remainder, 3600) - minutes, seconds = divmod(remainder, 60) - duration = ('%sd %sh %sm %ss' % - (days, hours, minutes, seconds)) - self.active_trades[tradeId]['duration'] = duration.replace( - "0d ", "").replace("0h ", "").replace("0m ", "") - except Exception as e: - pass - - trade = self.active_trades[tradeId] - if trade['entry_price'] > 0 and trade['position'] == 0: - if trade['direction'] == "SELL": - pnl = trade['entry_price'] - trade['exit_price'] - else: - pnl = trade['exit_price'] - trade['entry_price'] - - pnl = utils.to_decimal(pnl) - # print("1)", pnl) - self.active_trades[tradeId]['realized_pnl'] = pnl - - # print("\n\n-----------------") - # print(self.active_trades[tradeId]) - # print("-----------------\n\n") - - # get trade - trade = self.active_trades[tradeId].copy() - - # rename trade direction - trade['direction'] = trade['direction'].replace( - "BUY", "LONG").replace("SELL", "SHORT") - - # log - self.log_trade(trade) - - # remove from active trades and add to trade - if trade['action'] == "EXIT": - del self.active_trades[tradeId] - self.trades.append(trade) - - # return trade - return trade # --------------------------------------- def log_trade(self, trade): @@ -783,64 +655,6 @@ def get_orders(self, symbol): return {} - # --------------------------------------- - def get_positions(self, symbol): - symbol = self.get_symbol(symbol) - - if self.backtest: - position = 0 - avgCost = 0.0 - - if self.datastore.recorded is not None: - data = self.datastore.recorded - col = symbol.upper() + '_POSITION' - position = data[col].values[-1] - if position != 0: - pos = data[col].diff() - avgCost = data[data.index.isin(pos[pos != 0][-1:].index) - ][symbol.upper() + '_OPEN'].values[-1] - return { - "symbol": symbol, - "position": position, - "avgCost": avgCost, - "account": "Backtest" - } - - elif symbol in self.broker.positions: - return self.broker.positions[symbol] - - return { - "symbol": symbol, - "position": 0, - "avgCost": 0.0, - "account": None - } - - # --------------------------------------- - def get_portfolio(self, symbol=None): - raise Exception("Not supported") - # if symbol is not None: - # symbol = self.get_symbol(symbol) - # - # if symbol in self.zerodha.portfolio: - # portfolio = self.zerodha.portfolio[symbol] - # if "symbol" in portfolio: - # return portfolio - # - # return { - # "symbol": symbol, - # "position": 0.0, - # "marketPrice": 0.0, - # "marketValue": 0.0, - # "averageCost": 0.0, - # "unrealizedPNL": 0.0, - # "realizedPNL": 0.0, - # "totalPNL": 0.0, - # "account": None - # } - # - # return self.zerodha.portfolio - # --------------------------------------- def get_pending_orders(self, symbol=None): if symbol is not None: diff --git a/kinetick/lib/brokers/webull/webull_wrapper.py b/kinetick/lib/brokers/webull/webull_wrapper.py index ec632f1..1ac5d62 100644 --- a/kinetick/lib/brokers/webull/webull_wrapper.py +++ b/kinetick/lib/brokers/webull/webull_wrapper.py @@ -45,19 +45,14 @@ # ============================================= +""" + The idea of this class is to facilitate + 1. creation of contract objects based on csv spec. - exposes couple of create contract & register symbol methods which are used by blotter. + 2. Establish stream connection with ticker data provider. + 3. Populate DataFrame objects for quotes/ticks on receiving messages from broker and relay the message to blotter. +""" class Webull: - # ----------------------------------------- - @staticmethod - def roundClosestValid(val, res=0.01, decimals=None): - if val is None: - return None - """ round to closest resolution """ - if decimals is None and "." in str(res): - decimals = len(str(res).split('.')[1]) - - return round(round(val / res) * res, decimals) - # ----------------------------------------- def __init__(self, paper=False): """Initialize a new webull object.""" @@ -133,24 +128,10 @@ def __init__(self, paper=False): # trailing stops self.trailingStops = {} - # "tickerId" = { - # orderId: ... - # lastPrice: ... - # trailPercent: ... - # trailAmount: ... - # quantity: ... - # } + # triggerable trailing stops self.triggerableTrailingStops = {} - # "tickerId" = { - # parentId: ... - # stopOrderId: ... - # triggerPrice: ... - # trailPercent: ... - # trailAmount: ... - # quantity: ... - # } # holds options data optionsDF = DataFrame({ @@ -224,18 +205,6 @@ def disconnect(self): self.connected = False self.started = False - # ----------------------------------------- - def getServerTime(self): - """ get the current time on Server """ - self.time = datetime.utcnow() - - # ----------------------------------------- - - # ----------------------------------------- - def getAccountDetails(self): - """ get the current user details """ - self.wb.get_account() - # ----------------------------------------- @staticmethod def contract_to_dict(contract): @@ -425,190 +394,6 @@ def handleContractDetails(self, msg, end=False): # fire callback self.callbacks(caller="handleContractDetails", msg=msg) - # ----------------------------------------- - # Account handling - # ----------------------------------------- - def handleAccount(self, msg): - """ - handle account info update - Obsolete. - """ - - # parse value - try: - msg.value = float(msg.value) - except Exception: - msg.value = msg.value - if msg.value in ['true', 'false']: - msg.value = (msg.value == 'true') - - try: - # log handler msg - self.log_msg("account", msg) - - # new account? - if msg.accountName not in self._accounts.keys(): - self._accounts[msg.accountName] = {} - - # set value - self._accounts[msg.accountName][msg.key] = msg.value - - # fire callback - self.callbacks(caller="handleAccount", msg=msg) - except Exception: - pass - - def _get_active_account(self, account): - account = None if account == "" else None - if account is None: - if self.default_account is not None: - return self.default_account - elif len(self._accounts) > 0: - return self.accountCodes[0] - return account - - @property - def accounts(self): - return self._accounts - - @property - def account(self): - return self.getAccount() - - @property - def accountCodes(self): - return list(self._accounts.keys()) - - @property - def accountCode(self): - return self.accountCodes[0] - - def getAccount(self, account=None): - if len(self._accounts) == 0: - return {} - - account = self._get_active_account(account) - - if account is None: - if len(self._accounts) > 1: - raise ValueError("Must specify account number as multiple accounts exists.") - return self._accounts[list(self._accounts.keys())[0]] - - if account in self._accounts: - return self._accounts[account] - - raise ValueError("Account %s not found in account list" % account) - - # ----------------------------------------- - # Position handling - # ----------------------------------------- - def handlePosition(self, msg): - """ handle positions changes """ - - # log handler msg - self.log_msg("position", msg) - - # contract identifier - contract_tuple = self.contract_to_tuple(msg.contract) - contractString = self.contractString(contract_tuple) - - # try creating the contract - self.registerContract(msg.contract) - - # new account? - if msg.account not in self._positions.keys(): - self._positions[msg.account] = {} - - # if msg.pos != 0 or contractString in self.contracts.keys(): - self._positions[msg.account][contractString] = { - "symbol": contractString, - "position": int(msg.pos), - "avgCost": float(msg.avgCost), - "account": msg.account - } - - # fire callback - self.callbacks(caller="handlePosition", msg=msg) - - @property - def positions(self): - return self.getPositions() - - def getPositions(self, account=None): - if len(self._positions) == 0: - return {} - - account = self._get_active_account(account) - - if account is None: - if len(self._positions) > 1: - raise ValueError("Must specify account number as multiple accounts exists.") - return self._positions[list(self._positions.keys())[0]] - - if account in self._positions: - return self._positions[account] - - raise ValueError("Account %s not found in account list" % account) - - # ----------------------------------------- - # Portfolio handling - # ----------------------------------------- - def handlePortfolio(self, msg): - """ handle portfolio updates """ - - # log handler msg - self.log_msg("portfolio", msg) - - # contract identifier - contract_tuple = self.contract_to_tuple(msg.contract) - contractString = self.contractString(contract_tuple) - - # try creating the contract - self.registerContract(msg.contract) - - # new account? - if msg.accountName not in self._portfolios.keys(): - self._portfolios[msg.accountName] = {} - - self._portfolios[msg.accountName][contractString] = { - "symbol": contractString, - "position": int(msg.position), - "marketPrice": float(msg.marketPrice), - "marketValue": float(msg.marketValue), - "averageCost": float(msg.averageCost), - "unrealizedPNL": float(msg.unrealizedPNL), - "realizedPNL": float(msg.realizedPNL), - "totalPNL": float(msg.realizedPNL) + float(msg.unrealizedPNL), - "account": msg.accountName - } - - # fire callback - self.callbacks(caller="handlePortfolio", msg=msg) - - @property - def portfolios(self): - return self._portfolios - - @property - def portfolio(self): - return self.getPortfolio() - - def getPortfolio(self, account=None): - if len(self._portfolios) == 0: - return {} - - account = self._get_active_account(account) - - if account is None: - if len(self._portfolios) > 1: - raise ValueError("Must specify account number as multiple accounts exists.") - return self._portfolios[list(self._portfolios.keys())[0]] - - if account in self._portfolios: - return self._portfolios[account] - - raise ValueError("Account %s not found in account list" % account) - # ----------------------------------------- # Order handling # ----------------------------------------- @@ -878,113 +663,12 @@ def handleTickString(self, msg): ts = dateutil.parser.parse(data['tradeTime']) \ .strftime(COMMON_TYPES["DATE_TIME_FORMAT_LONG_MILLISECS"]) df2use[tickerId].index = [ts] - # self.log.debug("[TICK TS]: %s", ts) - # handle trailing stop orders - # if self.contracts[msg.tickerId].m_secType not in ("OPT", "FOP"): - # self.triggerTrailingStops(msg.tickerId) - # self.handleTrailingStops(msg.tickerId) # fire callback self.callbacks(caller="handleTickString", msg=msg) - # elif (msg.tickType == TYPES["FIELD_RTVOLUME"]): - # - # # log handler msg - # # self.log_msg("rtvol", msg) - # - # tick = dict(TYPES["RTVOL_TICKS"]) - # (tick['price'], tick['size'], tick['time'], tick['volume'], - # tick['wap'], tick['single']) = msg.value.split(';') - # - # try: - # tick['last'] = float(tick['price']) - # tick['lastsize'] = float(tick['size']) - # tick['volume'] = float(tick['volume']) - # tick['wap'] = float(tick['wap']) - # tick['single'] = tick['single'] == 'true' - # tick['instrument'] = self.tickerSymbol(msg.tickerId) - # - # # parse time - # s, ms = divmod(int(tick['time']), 1000) - # tick['time'] = '{}.{:03d}'.format( - # time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(s)), ms) - # - # # add most recent bid/ask to "tick" - # tick['bid'] = df2use[msg.tickerId]['bid'][0] - # tick['bidsize'] = int(df2use[msg.tickerId]['bidsize'][0]) - # tick['ask'] = df2use[msg.tickerId]['ask'][0] - # tick['asksize'] = int(df2use[msg.tickerId]['asksize'][0]) - # - # # self.log.debug("%s: %s\n%s", tick['time'], self.tickerSymbol(msg.tickerId), tick) - # - # # fire callback - # self.ibCallback(caller="handleTickString", msg=msg, tick=tick) - # - # except Exception: - # pass - - # else: - # # self.log.info("tickString-%s", msg) - # # fire callback - # self.ibCallback(caller="handleTickString", msg=msg) - - # print(msg) - - # ----------------------------------------- - def handleTickOptionComputation(self, msg): - """ - holds latest option data timestamp - only option price is kept at the moment - https://www.interactivebrokers.com/en/software/api/apiguide/java/tickoptioncomputation.htm - """ - - def calc_generic_val(data, field): - last_val = data['last_' + field].values[-1] - bid_val = data['bid_' + field].values[-1] - ask_val = data['ask_' + field].values[-1] - bid_ask_val = last_val - if bid_val != 0 and ask_val != 0: - bid_ask_val = (bid_val + ask_val) / 2 - return max([last_val, bid_ask_val]) - def valid_val(val): - return float(val) if val < 1000000000 else None - - # create tick holder for ticker - if msg._tickerId not in self.optionsData.keys(): - self.optionsData[msg._tickerId] = self.optionsData[0].copy() - - col_prepend = "" - if msg.field == "FIELD_BID_OPTION_COMPUTATION": - col_prepend = "bid_" - elif msg.field == "FIELD_ASK_OPTION_COMPUTATION": - col_prepend = "ask_" - elif msg.field == "FIELD_LAST_OPTION_COMPUTATION": - col_prepend = "last_" - - # save side - self.optionsData[msg._tickerId][col_prepend + 'imp_vol'] = valid_val(msg.impliedVol) - self.optionsData[msg._tickerId][col_prepend + 'dividend'] = valid_val(msg.pvDividend) - self.optionsData[msg._tickerId][col_prepend + 'delta'] = valid_val(msg.delta) - self.optionsData[msg._tickerId][col_prepend + 'gamma'] = valid_val(msg.gamma) - self.optionsData[msg._tickerId][col_prepend + 'vega'] = valid_val(msg.vega) - self.optionsData[msg._tickerId][col_prepend + 'theta'] = valid_val(msg.theta) - self.optionsData[msg._tickerId][col_prepend + 'price'] = valid_val(msg.optPrice) - - # save generic/mid - data = self.optionsData[msg._tickerId] - self.optionsData[msg._tickerId]['imp_vol'] = calc_generic_val(data, 'imp_vol') - self.optionsData[msg._tickerId]['dividend'] = calc_generic_val(data, 'dividend') - self.optionsData[msg._tickerId]['delta'] = calc_generic_val(data, 'delta') - self.optionsData[msg._tickerId]['gamma'] = calc_generic_val(data, 'gamma') - self.optionsData[msg._tickerId]['vega'] = calc_generic_val(data, 'vega') - self.optionsData[msg._tickerId]['theta'] = calc_generic_val(data, 'theta') - self.optionsData[msg._tickerId]['price'] = calc_generic_val(data, 'price') - self.optionsData[msg._tickerId]['underlying'] = valid_val(msg.undPrice) - - # fire callback - self.callbacks(caller="handleTickOptionComputation", msg=msg) # ----------------------------------------- # trailing stops