Ejemplo n.º 1
0
class LiveTrading(MethodManager_):
    def __init__(self, strat_name, client_id, runtime_tm, debugging, manager_,
                 heartbeat_q, traded_instr):
        self.strat_name = strat_name
        self.client_id = client_id
        self.runtime_tm = runtime_tm
        self.debugging = debugging
        self.manager_ = manager_
        self.heartbeat_q = heartbeat_q
        self.traded_instr = traded_instr

        self.account_curr = ACCOUNT_CURR
        self.tax_rate_account_country = TAX_RATE_ACCOUNT_COUNTRY
        self.simult_reqs_interval = SIMULT_REQS_INTERVAL
        self.potential_mkt_data_lines_already_in_use = POTENTIAL_MKT_DATA_LINES_ALREADY_IN_USE

        self.ib = IB()
        # self.ib.setCallback('error', self.on_ib_error)
        self.ib.errorEvent += self.on_ib_error
        self.err_ = None

        self.report = Reporting(self)
        self.cash = CashManagement(debugging)
        self.hlprs = ClientSharedMemory()

        self.access_type = 'tws'
        if self.access_type == 'gateway':
            self.__port = 4001
        elif self.access_type == 'tws':
            self.__port = 7497
        self.__host = '127.0.0.1'
        self.ib.connect(self.__host, self.__port, clientId=self.client_id)

        self.portfolio = []
        self.portfolio_instr = []
        self.cnt_trades_per_instr_per_day = {}

        self.req_tracker = {
            'total': 0,
            'open_market_data_reqs': 0,
            'open_market_data_lines': 0,
            'hist_data_prior_time': None
        }

        self.start_cet = datetime.datetime.now()

        self.manager_[
            'active_mkt_data_lines'] = POTENTIAL_MKT_DATA_LINES_ALREADY_IN_USE
        self.funda_data_req_max = IB_PACING['funda_data_reqs']['reqs']

        if self.debugging.get_meths:
            self.print_meths(str(self.__class__.__name__), dir(self))

        _inst_ = self
        self.fin_ratios = FinRatios(_inst_)

    @staticmethod
    def us_tz_op(dt_obj):
        """
        Converting time
        :param dt_obj: datetime obj
        :return: Eastern datetime
        """
        # log_start = datetime.datetime.now()
        if not isinstance(dt_obj, datetime.datetime):
            raise TypeError
        if dt_obj.year == 1900:
            curr_year = datetime.datetime.now().year
        else:
            curr_year = dt_obj.year
        # http://www.webexhibits.org/daylightsaving/b2.html
        dst = {
            2018: [[3, 11], [11, 4]],
            2019: [[3, 10], [11, 3]],
            2020: [[3, 8], [11, 1]]
        }
        window = dst[curr_year]

        #disclaimer
        kind_of_difference = HOUR_DAY_BOUNDARY_FAIL_CHECK
        if dt_obj.hour < kind_of_difference:
            raise Exception("determination difficult, try later in the day")

        dst_start = datetime.datetime(curr_year, window[0][0], window[0][1])
        dst_end = datetime.datetime(curr_year, window[1][0], window[1][1])
        if dt_obj > dst_start and dt_obj < dst_end:
            utc2est = -4
        else:
            utc2est = -5
        this_tz_from_uts = '+0100'
        if time.strftime("%z", time.gmtime()) != this_tz_from_uts:
            raise exceptions_.WrongTimeZone(
                'Wrong time zone, code is fixed to Danish winter time, might have to be adjusted'
            )

        cet2utc = -2
        dt_obj_utc = dt_obj + datetime.timedelta(
            hours=cet2utc)  #.replace(tzinfo = datetime.timezone.utc)
        dt_obj_est = dt_obj_utc + datetime.timedelta(hours=utc2est)
        # logging = rk_logging.Logging()
        # logging.log(
        #         runtime_tm, log_start, str('' + ',' + sys._getframe().f_code.co_name)
        #         ,dt_obj_utc
        #         ,dt_obj_est
        #     )
        return dt_obj_est

    def _manipulated_time(self, cet_in):
        delta = cet_in - self.start_cet
        current_manipulated_time = datetime.datetime(
            self.start_cet.year, self.start_cet.month, self.start_cet.day,
            self.debugging.dummy_hour, self.debugging.dummy_minute,
            self.debugging.dummy_second)
        current_manipulated_time += delta
        return current_manipulated_time

    def buy(self, ticker, rank, size, order_type):
        log_start = datetime.datetime.now()
        log = logging_.Logging(
            self.runtime_tm, log_start,
            str(self.__class__.__name__ + ',' +
                sys._getframe().f_code.co_name))
        self.hlprs.add_to_manager(self.strat_name, *log.monitor())
        order_log_msg = None
        symbol = ticker.contract.symbol
        if self._ticker_len_n_type_check(ticker.domBids) > 1:
            offer_price = ticker.domBids[rank].price
            # offer_size = ticker.domBids[rank].size
        else:
            offer_price = ticker.domBids.price
            # offer_size = ticker.domBids.size
        cnt = 0
        max_order_filled_cnts = int(ORDER_CANCEL_IF_NOT_FILLED_SECS /
                                    ORDER_FILLED_CHECKED_CYCLE_SECS)
        if self.debugging.dummy_data:
            order_success = True
        else:
            if order_type == 'MKT':
                order = MarketOrder('BUY', size)
                trade = self.ib.placeOrder(ticker.contract, order)
                while cnt < max_order_filled_cnts:  # a bit complicated but it does actually make sense
                    order_log_msg = trade.log
                    self.ib.sleep(ORDER_FILLED_CHECKED_CYCLE_SECS)
                    cnt += 1
                    if not trade.orderStatus.status != 'Filled':
                        # PendingSubmit = 'PendingSubmit'
                        # PendingCancel = 'PendingCancel'
                        # PreSubmitted = 'PreSubmitted'
                        # Submitted = 'Submitted'
                        # ApiPending = 'ApiPending'  # undocumented, can be returned from req(All)OpenOrders
                        # ApiCancelled = 'ApiCancelled'
                        # Cancelled = 'Cancelled'
                        # Filled = 'Filled'
                        # Inactive = 'Inactive'
                        order_success = True
                        break
                else:
                    self.ib.cancelOrder(trade)
                # while not trade.isDone():
                #     print("not sure if this makes sense")
                #     self.ib.waitOnUpdate()
            elif order_type == 'LMT':
                raise Exception("order type not defined")
            else:
                raise Exception("order type not defined")
        if order_success:
            if self.debugging.dummy_time:
                _now = self._manipulated_time(datetime.datetime.now())
            else:
                _now = datetime.datetime.now()

            filled_tm = self.us_tz_op(_now)
            status = 0
            self.portfolio.append([symbol, filled_tm, offer_price, size])
            self.hlprs.add_to_manager(self.strat_name, 'portfolio',
                                      self.portfolio)

            self.portfolio_instr.append(symbol)
            # self.add_to_monitor('portfolio_instr', 'self.portfolio_instr')

            print('not sure if this works')
            self.cnt_trades_per_instr_per_day[ticker.contract.symbol] += 1
            self.hlprs.add_to_manager(self.strat_name,
                                      'cnt_trades_per_instr_per_day',
                                      self.cnt_trades_per_instr_per_day)
            self.heartbeat_q.put([
                self.strat_name, 'cnt_trades_per_instr_per_day',
                self.cnt_trades_per_instr_per_day
            ])
            self.hlprs.add_to_manager(
                self.strat_name, 'cap_usd',
                self.cash.available_funds(self.report.pnl(),
                                          self.account_curr))
        else:
            status = 1
        log.log(self.portfolio, self.portfolio_instr,
                self.cnt_trades_per_instr_per_day, order_log_msg)
        return status

    def sell(self, ticker, rank, order_type):
        log_start = datetime.datetime.now()
        log = logging_.Logging(
            self.runtime_tm, log_start,
            str(self.__class__.__name__ + ',' +
                sys._getframe().f_code.co_name))
        self.hlprs.add_to_manager(self.strat_name, *log.monitor())
        order_log_msg = None
        symbol = ticker.contract.symbol
        try:
            ix = self.portfolio_instr.index(symbol)
        except ValueError:
            status = -1
            print(
                'not quite sure why this error sometimes occurs and if this is maybe to early to leave?'
            )
            return status

        # if isinstance(ticker.domBids, str) == True:
        #     offer_price = ticker.domAsks.price
        #     offer_size = ticker.domAsks.size
        # else:
        #     offer_price = ticker.domAsks[rank].price
        #     offer_size = ticker.domAsks[rank].size
        price_ix = 2
        size_ix = 3
        # held_price = self.held[ix][price_ix]
        held_size = self.portfolio[ix][size_ix]

        cnt = 0
        max_order_filled_cnts = int(ORDER_CANCEL_IF_NOT_FILLED_SECS /
                                    ORDER_FILLED_CHECKED_CYCLE_SECS)
        if not self.debugging.dummy_data:
            if order_type == 'MKT':
                order = MarketOrder('SELL', held_size)
                # https://github.com/erdewit/ib_insync/blob/master/notebooks/ordering.ipynb
                trade = self.ib.placeOrder(ticker.contract, order)
                while cnt < max_order_filled_cnts:
                    order_log_msg = trade.log
                    self.ib.sleep(ORDER_FILLED_CHECKED_CYCLE_SECS)
                    cnt += 1
                    if trade.orderStatus.status == 'Filled':
                        order_success = True
                        break
                self.ib.cancelOrder(trade)
                while not trade.isDone():
                    print("not sure if this makes sense")
                    self.ib.waitOnUpdate()
            elif order_type == 'LMT':
                raise Exception("order type not defined")
            else:
                raise Exception("order type not defined")
        else:
            order_success = True
        order_success = True
        if order_success:
            status = 0
            del self.portfolio[ix]
            self.hlprs.add_to_manager(self.strat_name, 'portfolio',
                                      self.portfolio)
            del self.portfolio_instr[ix]
            # self.add_to_monitor('portfolio_instr', self.portfolio_instr)
            print('not sure if this works')
            self.cnt_trades_per_instr_per_day[ticker.contract.symbol] += 1
            self.hlprs.add_to_manager(self.strat_name,
                                      'cnt_trades_per_instr_per_day',
                                      self.cnt_trades_per_instr_per_day)
            self.heartbeat_q.put([
                self.strat_name, 'cnt_trades_per_instr_per_day',
                self.cnt_trades_per_instr_per_day
            ])
            self.hlprs.add_to_manager(
                self.strat_name, 'cap_usd',
                self.cash.available_funds(self.report.pnl(),
                                          self.account_curr))
        else:
            status = 1
        log.log(self.portfolio, self.portfolio_instr,
                self.cnt_trades_per_instr_per_day, order_log_msg)
        return status

    @staticmethod
    def _ticker_len_n_type_check(bid_or_ask_li):
        # log_start = datetime.datetime.now()
        n_ticks = len(bid_or_ask_li) if isinstance(bid_or_ask_li,
                                                   str) == False else 1
        # log = rk_logging.Logging()
        # log.log(
        #     self.runtime_tm, log_start, str('' + ',' + sys._getframe().f_code.co_name)
        # )
        return n_ticks

    def req_handling(self, ticker, type_):
        log_start = datetime.datetime.now()
        log = logging_.Logging(
            self.runtime_tm, log_start,
            str(self.__class__.__name__ + ',' +
                sys._getframe().f_code.co_name))
        self.hlprs.add_to_manager(self.strat_name, *log.monitor())
        # unix_ts = int(time.time())
        self.req_tracker['total'] += 1
        self.req_tracker['open_market_data_reqs'] += 1
        self.req_tracker['open_market_data_lines'] += 1

        # status = -1 means immediately kill connections
        status = self.pacing_violations(type_)
        log.log()
        if status == -1:
            print('get connections here')
            # do sth
            return
        elif status == 0:
            return

    def req_handler(self, toggle):
        if toggle == 'increase':
            self.manager_['active_mkt_data_lines'] += 1
        elif toggle == 'decrease':
            self.manager_['active_mkt_data_lines'] -= 1
        else:
            raise Exception('not defined')

    def req_mkt_dpt_ticker_(self, contract_):
        _cnt_ = 0
        status = 1
        while _cnt_ < N_TRIES_IF_SIMULT_REQS:
            if self.manager_['active_mkt_data_lines'] < IB_PACING[
                    'mkt_data_lines']:
                self.req_handler('increase')
                ticker = self.ib.reqMktDepth(contract_)
                self.ib.sleep(
                    SIMULT_REQS_INTERVAL
                )  # wait here because ticker needs updateEvent might not be fast enough
            else:
                self.ib.sleep(
                    SIMULT_REQS_INTERVAL
                )  # wait here because ticker needs updateEvent might not be fast enough
                _cnt_ += 1
                continue
            if self.err_ != exceptions_.IbPacingError:
                status = 0
                break
        return ticker, status

    def cancel_mkt_dpt_ticker_(self, contract_):
        self.ib.cancelMktDepth(contract_)
        self.req_handler('decrease')

    @staticmethod
    def _get_ib_pacing():
        return IB_PACING

    def pacing_violations(self, type_):
        log_start = datetime.datetime.now()
        log = logging_.Logging(
            self.runtime_tm, log_start,
            str(self.__class__.__name__ + ',' +
                sys._getframe().f_code.co_name))
        self.hlprs.add_to_manager(self.strat_name, *log.monitor())
        # histData, mktDepth, scannerData

        # TODO: far from done here, continue at some point
        status = 0
        if type_ == 'histData':
            if self.req_tracker[
                    'total'] == 1:  #first time is already incremented
                self.req_tracker['hist_data_prior_time'] = int(time.time())
                status = 0
            elif self.req_tracker['total'] > 1 and self.req_tracker[
                    'open_market_data_lines'] < MARKET_DATA_LINES:
                status = 0
            elif self.req_tracker['total'] > 1 \
                    and self.req_tracker['open_market_data_lines'] == MARKET_DATA_LINES \
                    and int(time.time()) - self.req_tracker['hist_data_prior_time'] >= IB_PACING['hist_data_similar']['secs']:
                print('CRITICAL: market data lines limit reached')
                status = -1
        elif type_ == 'mktDepth':
            status = 0
        elif type_ == 'scannerData':
            status = 0
        log.log(status, self.req_tracker)
        self.hlprs.add_to_manager(self.strat_name, *log.monitor())
        return status

    def check_network_requirements(self):
        # s = socket.socket()
        # #e.g.
        # address, port = '10.8.8.19', '53141'
        # address, port = '208.245.107.3', '4000'
        # try:
        #     s.connect((address, port))
        #     return True
        # except socket.error:
        #     return False
        # #or
        # rows = []
        # lc = psutil.net_connections('inet')
        # for c in lc:
        #     (ip, port) = c.laddr  # 0.0.0.0
        #     if ip == '10.8.8.19':  # or ip == '::'
        #         if c.type == socket.SOCK_STREAM and c.status == psutil.CONN_LISTEN:
        #             proto_s = 'tcp'
        #         elif c.type == socket.SOCK_DGRAM:
        #             proto_s = 'udp'
        #         else:
        #             continue
        #         pid_s = str(c.pid) if c.pid else '(unknown)'
        #         msg = 'PID {} is listening on port {}/{} for all IPs.'
        #         msg = msg.format(pid_s, port, proto_s)
        #         print(msg)
        status = True
        if not status:
            raise Exception('cannot connect to all necessary servers')
#

    def on_ib_error(self, reqId, errorCode, errorString, errorSomething):
        """
        https://groups.io/g/insync/topic/how_to_capture_error_trapped/7718294?p=,,,20,0,0,0::recentpostdate%2Fsticky,,,20,1,80,7718294
        """
        max_n_mkt_depth_reqs = 309  # ERROR:ib_insync.wrapper:Error 309, reqId 39: Max number (3) of market depth requests has been reached, contract: Stock(symbol='PETZ', exchange='ISLAND', currency='USD')
        if errorCode == max_n_mkt_depth_reqs:
            self.err_ = exceptions_.IbPacingError
Ejemplo n.º 2
0
class IbManager(object):
    log_file = 'ib_manager'

    def __init__(self, ip: str, port: int, client_id: int):
        self._ib = IB()
        self._ib_ip: str = ip
        self._ib_port: int = port
        self._client_id: int = client_id
        self._subscribed_mkt_contracts: List[str] = []
        self._subscribed_mkt_depth_contracts: List[str] = []
        self._log: Log = Log.create(Log.path(self.log_file))
        self._logger = self._log.get_logger('ibmanager')
        self._recorder: Recorder = Recorder(self._log)
        self._market_recorder: MarketRecorder = MarketRecorder(
            self._ib, self._recorder)
        self._account_recorder: AccountRecorder = AccountRecorder(
            self._ib, self._recorder)
        self._keep_connection_task: asyncio.Task = None
        self._ib.connectedEvent += self.on_ib_connected
        self._ib.disconnectedEvent += self.on_ib_disconnected
        self._reconnect_flag: bool = False

    def on_ib_connected(self) -> None:
        self._logger.info('connected with ib')
        self._reconnect_flag = False
        self._recover_subscriptions()

    def on_ib_disconnected(self) -> None:
        self._logger.warning('disconnected with ib')
        self._reconnect_flag = True
        if self._keep_connection_task is None:
            self._keep_connection_task = asyncio.create_task(self._reconnect())

    async def _reconnect(self) -> None:
        while self._reconnect_flag:
            await asyncio.sleep(20)
            self._logger.info('try to reconnect ib gateway')
            await self.initialize()
        self._keep_connection_task = None

    def _recover_subscriptions(self) -> None:
        for contract in self._subscribed_mkt_contracts:
            self._logger.info(f'recover subscribe {str(contract)}')
            self._ib.reqMktData(contract)
        for contract in self._subscribed_mkt_depth_contracts:
            self._logger.info(f'recover subscribe depth {str(contract)}')
            self._ib.reqMktDepth(contract)

    async def initialize(self):
        if self._ib.isConnected():
            return
        try:
            await self._ib.connectAsync(self._ib_ip,
                                        self._ib_port,
                                        clientId=self._client_id)
            accounts = self._ib.managedAccounts()
            if len(accounts) > 0:
                self._account_recorder.update_account(accounts[0])
                self.update_account()
        except Exception:
            pass

    def update_account(self):
        self._ib.reqAccountSummaryAsync()

    async def find_symbols(self, pattern: str) -> List[str]:
        symbols = await self._ib.reqMatchingSymbolsAsync(pattern)
        contracts = [symbol.contract.nonDefaults() for symbol in symbols]
        return contracts

    def make_contract(self, **kwargs) -> Contract:
        return Contract.create(**kwargs)

    def sub_market(self, contract: Contract) -> str:
        if contract in self._subscribed_mkt_contracts:
            return 'already subscribe {}'.format(str(contract))
        self._subscribed_mkt_contracts.append(contract)
        self._ib.reqMktData(contract)
        return 'subscribe {} success'.format(str(contract))

    def unsub_market(self, contract: Contract) -> str:
        if contract not in self._subscribed_mkt_contracts:
            return 'not ever subscribe {}'.format(str(contract))
        self._subscribed_mkt_contracts.append(contract)
        self._ib.cancelMktData(contract)
        return 'unsubscribe {} success'.format(str(contract))

    def sub_market_depth(self, contract: Contract) -> str:
        if contract in self._subscribed_mkt_depth_contracts:
            return 'already subscribe depth {}'.format(str(contract))
        self._subscribed_mkt_depth_contracts.append(contract)
        self._ib.reqMktDepth(contract)
        return 'subscribe depth {} success'.format(str(contract))

    def unsub_market_depth(self, contract: Contract) -> str:
        if contract not in self._subscribed_mkt_depth_contracts:
            return 'not ever subscribe depth {}'.format(str(contract))
        self._subscribed_mkt_contracts.remove(contract)
        self._ib.cancelMktDepth(contract)
        return 'unsubscribe depth {} success'.format(str(contract))

    def place_order(self, contract: Contract, side: str, size: int,
                    price: float) -> str:
        trade = self._place_order(contract, side, size, price)
        return str(trade)

    def _place_order(self, contract: Contract, side: str, size: int,
                     price: float) -> Trade:
        side = side.upper()
        if side not in ('SELL', 'BUY'):
            return [f'invalid order type: {side}']
        price = float(f'{round(float(price), 3):.3f}')
        order = LimitOrder(side, size, price, tif='GTC')
        trade = self._ib.placeOrder(contract, order)
        return trade

    def cancel_order(self, order_id: int) -> str:
        order_id = int(order_id)
        order = Order(orderId=order_id)
        trade = self._ib.cancelOrder(order)
        return str(trade)

    async def orders(self) -> List[str]:
        orders = await self._ib.reqOpenOrdersAsync()
        return [str(order) for order in orders]

    def portfolio(self) -> List[str]:
        results = self._ib.portfolio()
        return [str(value) for value in results]