Beispiel #1
0
    def _fill_order(self, order: OrderCommon, qty_to_fill, price_fill, fee=0.):
        assert price_fill >= 0, "Price has to be positive"
        assert abs(order.leaves_qty
                   ) >= 1, "Order with leaves_qty < 1 should be closed"
        assert abs(order.signed_qty
                   ) >= 1, "Order signed_qty should not be less than 1"
        assert abs(qty_to_fill) >= 1, "Can not fill order with qty less than 1"

        fully_filled = order.fill(qty_to_fill)
        side = order.side()
        fill = Fill(
            symbol=order.symbol,
            side=order.side_str(),
            qty_filled=qty_to_fill,
            price_fill=price_fill,
            fill_time=self.current_timestamp,
            fill_type=FillType.complete if fully_filled else FillType.partial,
            order_id=order.client_id)
        self._log_fill(fill)
        self.volume[order.symbol] += abs(qty_to_fill) * price_fill
        self.n_fills[order.symbol] += 1

        position = self.positions[order.symbol]
        position.update(signed_qty=qty_to_fill * side,
                        price=price_fill,
                        leverage=self.leverage[order.symbol],
                        current_timestamp=self.current_timestamp,
                        fee=fee)

        method = self._get_tactic(order.client_id).handle_fill
        self._queue_append(self.current_timestamp + WS_DELAY, method, fill)

        return fully_filled
Beispiel #2
0
    def _exec_market_order(self, order: OrderCommon):
        assert order.type == OrderType.Market
        assert order.status == OrderStatus.Pending
        order.status = OrderStatus.New
        tick_size = order.symbol.tick_size

        # TODO: watch for self-cross

        # We only fill at two price levels: best price and second best price
        if order.side() > 0:
            best_price = self.current_quote.ask_price
            second_best = best_price + tick_size
            size = self.current_quote.ask_size
        else:
            best_price = self.current_quote.bid_price
            second_best = best_price - tick_size
            size = self.current_quote.bid_size

        qty_fill1 = min(size, order.leaves_qty)
        qty_fill2 = order.leaves_qty - qty_fill1

        fully_filled = self._fill_order(order=order,
                                        qty_to_fill=qty_fill1,
                                        price_fill=best_price)
        if not fully_filled:
            assert qty_fill2 > 0
            self._fill_order(order=order,
                             qty_to_fill=qty_fill2,
                             price_fill=second_best)

        return
Beispiel #3
0
    def _create_orders(self, learning: float, fcst: float, quote: Quote):

        # TODO: improve to use full book information
        buy_best = quote.bid_price if quote.bid_size >= self.min_liq else quote.bid_price - 0.5
        sell_best = quote.ask_price if quote.ask_size >= self.min_liq else quote.ask_price + 0.5

        buy_best -= self.spread
        sell_best += self.spread

        price = fcst - learning / self.alpha

        buy = min(buy_best - 0.5, self._floor(price - 0.25))
        sell = max(sell_best + 0.5, self._ceil(price + 0.25))

        qty_buy = min(self.max_qty, round((price - buy) * self.alpha))
        qty_sel = min(self.max_qty, round((sell - price) * self.alpha))

        if qty_buy >= 1:
            self.buy_order = OrderCommon(symbol=self.symbol,
                                         type=OrderType.Limit,
                                         client_id=self.gen_order_id(),
                                         signed_qty=qty_buy,
                                         price=buy)

        if qty_sel >= 1:
            self.sell_order = OrderCommon(symbol=self.symbol,
                                          type=OrderType.Limit,
                                          client_id=self.gen_order_id(),
                                          signed_qty=-qty_sel,
                                          price=sell)
Beispiel #4
0
    def on_order(self, raw: dict, action: str):
        # DEV NOTE: this is the only method who can delete orders from self.all_orders
        # DEV NOTE: this is the only method who broadcast orders to tactics

        order_id = raw.get('clOrdID')

        if not order_id:
            self.logger.error(
                'Got an order without "clOrdID", probably a bitmex order (e.g., liquidation)'
            )
            self.logger.error('order: {}'.format(raw))
            raise AttributeError(
                'If we were liquidated, we should change our tactics')

        if OrderStatus[raw['ordStatus']] == OrderStatus.Rejected:
            raise ValueError(
                'A order should never be rejected, please fix the tactic. Order: {}'
                .format(raw))

        order = self.all_orders.get(order_id)

        if order:
            if action != 'update':
                raise AttributeError(
                    'Got action "{}". This should be an "update", since this order already '
                    'exist in our data and we don\'t expect a "delete"'.format(
                        action))

            if not order.is_open():
                # This is the case when a closed (e.g. Canceled) order where inserted, now we clean it up in the insert
                # We also assume that bitmex will not update a canceled/closed order twice
                del self.all_orders[order.client_id]
                return

            order.update_from_bitmex(raw)  # type: OrderCommon

            if not order.is_open():
                del self.all_orders[order.client_id]

                self._notify_cancel_if_the_case(order)

        else:
            symbol = Symbol[raw['symbol']]
            type_ = OrderType[raw['ordType']]
            order = OrderCommon(symbol=symbol,
                                type=type_).update_from_bitmex(raw)

            if action != 'insert' and order.is_open():
                raise AttributeError(
                    'Got action "{}". This should be an "insert", since this data does\'nt exist '
                    'in memory and is open. Order: {}'.format(action, raw))

            # yes, always add an order to the list, even if it is closed. If so, it will be removed in the "update" feed
            self.all_orders[order_id] = order

            self._notify_cancel_if_the_case(order)
Beispiel #5
0
 def _liq_pos(self, pos: float):
     self.exchange.send_orders([
         OrderCommon(symbol=self.symbol,
                     type=OrderType.Market,
                     client_id=self.gen_order_id(),
                     signed_qty=-pos)
     ])
Beispiel #6
0
    def send_orders(self, orders: List[OrderCommon]):
        # DEV NOTE: it logs all sent orders, good or bad orders
        # DEV NOTE: it should NOT change any internal state

        # sanity check
        if not all([o.status == OrderStatus.Pending for o in orders]):
            raise ValueError(
                "New orders should have status OrderStatus.Pending")

        for o in orders:
            if o.client_id:
                tokens = o.client_id.split('_')
                tactic_id = tokens[0]
            if not o.client_id or len(
                    tokens) < 2 or tactic_id not in self.tactics_map:
                raise ValueError(
                    'The order client_id should be a string in the form TACTICID_HASH'
                )

        converted = [
            self.convert_order_to_bitmex(self.check_order_sanity(order))
            for order in orders
        ]
        raws = self._curl_bitmex(path='order/bulk',
                                 postdict={'orders': converted},
                                 verb='POST')

        for r in raws:
            symbol = Symbol[r['symbol']]
            type_ = OrderType[r['ordType']]
            tactic_id = r['clOrdID'].split('_')[0]
            tactic = self.tactics_map[tactic_id]
            self._log_order(OrderCommon(symbol, type_).update_from_bitmex(r))
Beispiel #7
0
    def _init_files(self, log_dir):
        self.fills_file = open(os.path.join(log_dir, 'fills.csv'), 'w')
        self.orders_file = open(os.path.join(log_dir, 'orders.csv'), 'w')
        self.pnl_file = open(os.path.join(log_dir, 'pnl.csv'), 'w')

        self.fills_file.write(Fill.get_header() + '\n')
        self.orders_file.write(OrderCommon.get_header() + '\n')
        self.pnl_file.write('timestamp,symbol,pnl,cum_pnl\n')
Beispiel #8
0
    def _create_orders(self):
        quote = self.exchange.get_quote(self.symbol)
        bid = quote.bid_price
        ask = quote.ask_price

        # sending two orders at same time to check if we get two different fills
        self.orders_to_send = [
            OrderCommon(symbol=self.symbol,
                        type=OrderType.Limit,
                        price=bid - 0.5,
                        client_id=self.gen_order_id(),
                        signed_qty=+self.qty),
            OrderCommon(symbol=self.symbol,
                        type=OrderType.Limit,
                        price=ask + 0.5,
                        client_id=self.gen_order_id(),
                        signed_qty=-self.qty)
        ]
Beispiel #9
0
    def _create_orders(self):
        self.orders_to_send = []
        self.expected_position = []

        # step 0
        self.orders_to_send.append([
            OrderCommon(symbol=self.symbol,
                        type=OrderType.Market,
                        client_id=self.gen_my_id('a'),
                        signed_qty=-self.qty)
        ])
        self.expected_position.append(-self.qty)

        # step 1
        # sending two orders at same time to check if we get two different fills
        self.orders_to_send.append([
            OrderCommon(symbol=self.symbol,
                        type=OrderType.Market,
                        client_id=self.gen_my_id('b'),
                        signed_qty=-self.qty),
            OrderCommon(symbol=self.symbol,
                        type=OrderType.Market,
                        client_id=self.gen_my_id('c'),
                        signed_qty=-self.qty)
        ])
        self.expected_position.append(-3 * self.qty)

        # step 2
        self.orders_to_send.append([
            OrderCommon(symbol=self.symbol,
                        type=OrderType.Market,
                        client_id=self.gen_my_id('d'),
                        signed_qty=+4 * self.qty)
        ])
        self.expected_position.append(+self.qty)

        # step 3
        self.orders_to_send.append([
            OrderCommon(symbol=self.symbol,
                        type=OrderType.Market,
                        client_id=self.gen_my_id('e'),
                        signed_qty=-self.qty)
        ])
        self.expected_position.append(0)
Beispiel #10
0
    def _exec_limit_order(self, order: OrderCommon):
        assert order.type == OrderType.Limit

        bid = self.current_quote.bid_price
        ask = self.current_quote.ask_price
        order_side = order.side()

        self._check_price_sanity(order)

        violated_post_only = order_side > 0 and order.price >= ask or order_side < 0 and order.price <= bid
        if violated_post_only:
            order.status = OrderStatus.Canceled
            order.status_msg = OrderCancelReason.cross_during_post_only
            self._exec_order_cancels([order])
            return

        order.status = OrderStatus.New
        self.active_orders[order.client_id] = order
        order._made_spread = order_side > 0 and order.price > bid or order_side < 0 and order.price < ask
Beispiel #11
0
 def close_position(self, symbol: Symbol):
     # TODO: it should also close limit orders on the same side as the position
     pos = self.positions[symbol]
     oid = self.liquidator_tactic.gen_order_id().split('_')
     oid = oid[0] + '_closing_' + oid[1]
     if pos.is_open:
         self.send_orders([
             OrderCommon(symbol=symbol,
                         type=OrderType.Market,
                         client_id=oid,
                         signed_qty=-pos.signed_qty)
         ])
Beispiel #12
0
 def _execute_liquidation(self, symbol, order_cancel_reason=OrderCancelReason.liquidation):
     self.can_call_handles = False
     orders = filter_symbol(self.active_orders, symbol)
     cancelled = self._cancel_orders_helper(orders, reason=order_cancel_reason)
     self.active_orders = drop_orders(self.active_orders, cancelled)
     self.can_call_handles = True
     try:
         assert len(filter_symbol(self.active_orders, symbol)) == 0
     except:
         for i in filter_symbol(self.active_orders, symbol):
             print("-" + str(i[1].status))
         print("-" + str(len(cancelled)))
         raise AttributeError()
     position = self.positions.get(symbol, None)
     if not position or not position.is_open:
         return
     assert position.is_open
     order = OrderCommon(symbol=symbol,
                         signed_qty=-position.signed_qty,
                         type=OrderType.Market,
                         tactic=self.liquidator)
     order.status_msg = order_cancel_reason
     self.can_call_handles = False
     self.send_orders([order])
     self.can_call_handles = True
     assert order.status == OrderStatus.Filled
     if position.is_open:
         raise AttributeError("position was not close during liquidation. position = %f" % position.signed_qty)
     if not self.is_last_tick():
         self.n_liquidations[symbol.name] += 1
     if order_cancel_reason == OrderCancelReason.liquidation:
         closed = self.closed_positions_hist[symbol][-1]  # type: PositionSim
         if closed.realized_pnl >= 0:
             raise AttributeError("Liquidation caused profit! position = {},\n current price = {}"
                                  .format(str(position), self._estimate_price()))
     assert len(filter_symbol(self.active_orders, symbol)) == 0
Beispiel #13
0
    def handle_1m_candles(self, candles1m: pd.DataFrame) -> None:

        if not self.sell_id:
            opened_orders = self.exchange.get_opened_orders(self.symbol, self.id())
            if len(opened_orders) != 0:
                raise ValueError('This test has to start with no opened orders')

            # wait for quotes
            time.sleep(0.1)

            price = self.exchange.get_quote(self.symbol).ask_price
            if not price:
                raise AttributeError("We should have quote by now")

            orders = [
                # First order should be accepted
                OrderCommon(symbol=self.symbol,
                            type=OrderType.Limit,
                            client_id=self.gen_order_id(),
                            signed_qty=-self.qty,
                            price=price + 1000),

                # First order should be cancelled because we will try to sell bellow the bid
                # Note that our Limit orders should be post-only, i.e., ParticipateDoNotInitiate
                OrderCommon(symbol=self.symbol,
                            type=OrderType.Limit,
                            client_id=self.gen_order_id(),
                            signed_qty=-self.qty,
                            price=max(price - 500, 1)),
            ]

            self.sell_id = [o.client_id for o in orders]

            self.exchange.send_orders(orders)

        pass
Beispiel #14
0
    def get_opened_orders(self, symbol: Symbol,
                          client_id_prefix: str) -> OrderContainerType:

        raws = self._curl_bitmex(path='order',
                                 query={
                                     'symbol': symbol.name,
                                     'filter': '{"open": true}'
                                 },
                                 verb='GET')

        def is_owner(client_id: str):
            return client_id.split('_')[0] == client_id_prefix

        return {
            r['clOrdID']:
            OrderCommon(symbol, OrderType[r['ordType']]).update_from_bitmex(r)
            for r in raws if r['leavesQty'] != 0 and is_owner(r['clOrdID'])
        }
Beispiel #15
0
    def handle_1m_candles(self, candles1m: pd.DataFrame) -> None:
        if not self.candle_last_ts:
            self.candle_last_ts = candles1m.index[-1]
        elif not isinstance(candles1m, str):
            assert self.candle_last_ts < candles1m.index[-1]

        if self.n_trades > 0 and not self.buy_id[0] and self.n_positions > 0:
            logger.info("opening a position")
            self.initial_pos = self.exchange.get_position(
                self.symbol).signed_qty

            self.buy_id[0] = self.gen_order_id()
            self.next_action = 1
            logger.info("sending buy order {}".format(self.buy_id[0]))
            self.exchange.send_orders([
                OrderCommon(symbol=self.symbol,
                            type=OrderType.Market,
                            client_id=self.buy_id[0],
                            signed_qty=+self.qty)
            ])

        pass
Beispiel #16
0
    def print_output_files(self, input_args):
        if self.log_dir is None:
            raise ValueError("asked to print results, but log_dir is None")
        print("printing results to " + self.log_dir)
        fills_file = open(os.path.join(self.log_dir, 'fills.csv'), 'w')
        orders_file = open(os.path.join(self.log_dir, 'orders.csv'), 'w')
        pnl_file = open(os.path.join(self.log_dir, 'pnl.csv'), 'w')
        pars_used_file = open(os.path.join(self.log_dir, 'parameters_used'), 'w')
        summary_file = open(os.path.join(self.log_dir, 'summary'), 'w')

        fills_file.write(Fill.get_header() + '\n')
        orders_file.write(OrderCommon.get_header() + '\n')

        for f in self.fills_hist:  # type: Fill
            fills_file.write(f.to_line() + '\n')
        for o in self.order_hist:  # type: OrderCommon
            orders_file.write(o.to_line() + '\n')

        pnl_file.write('time,symbol,pnl,cum_pnl\n')
        for s in self.SYMBOLS:
            sum = 0
            for p in self.closed_positions_hist[s]:  # type: PositionSim
                sum += p.realized_pnl
                pnl_file.write(','.join([str(p.current_timestamp.strftime('%Y-%m-%dT%H:%M:%S')),
                                         s.name,
                                         str(p.realized_pnl),
                                         str(sum)])
                               + '\n')
        pars_used_file.write(str(input_args))
        pars_used_file.write("")

        summary_file.write(self.summary.to_str())

        pars_used_file.close()
        pnl_file.close()
        fills_file.close()
        orders_file.close()
        summary_file.close()
Beispiel #17
0
    def _exec_liquidation(self,
                          symbol: Symbol,
                          reason=OrderCancelReason.liquidation):
        orders_to_cancel = [
            o for o in self.active_orders.values() if o.symbol == symbol
        ]
        for order in orders_to_cancel:
            order.status_msg = reason
        self._exec_order_cancels(orders_to_cancel)

        if reason == OrderCancelReason.liquidation:
            self.n_liquidations[symbol] += 1

        if self.positions[symbol].is_open:
            order = OrderCommon(
                symbol=symbol,
                type=OrderType.Market,
                client_id=self.liquidator_tactic.gen_order_id(),
                signed_qty=-self.positions[symbol].signed_qty)

            self._exec_market_order(order)

        return
Beispiel #18
0
    def handle_fill(self, fill: Fill) -> None:

        if fill.side.lower()[0] == 'b':
            i = self.buy_id_to_index(fill.order_id)
            if i == -1:
                raise AttributeError("We didn't send order with id {}".format(
                    fill.order_id))
            self.buy_leaves[i] -= fill.qty

        else:
            if fill.side.lower()[0] != 's':
                raise AttributeError(
                    'side should be buy or sell, got {}'.format(fill.side))

            i = self.sell_id_to_index(fill.order_id)
            if i == -1:
                raise AttributeError("We didn't send order with id {}".format(
                    fill.order_id))
            self.sell_leaves[i] -= fill.qty

        if fill.fill_type == FillType.complete:
            # we need this little delays because it seems that bitmex takes a while to update the position
            time.sleep(.3)
            pos = self.exchange.get_position(self.symbol)
            assertEqual(
                pos.signed_qty, self.expected_position(),
                "n_buys={}, n_sells={}, init_pos={}".format(
                    min(self.next_action, self.n_trades),
                    max(self.next_action - self.n_trades, 0),
                    self.initial_pos))

            if self.next_action < self.n_trades:

                client_id = self.gen_order_id()
                self.buy_id[self.next_action] = client_id
                logger.info("sending buy order {}".format(
                    self.buy_id[self.next_action]))
                self.next_action += 1
                self.exchange.send_orders([
                    OrderCommon(symbol=self.symbol,
                                type=OrderType.Market,
                                client_id=client_id,
                                signed_qty=+self.qty)
                ])

            elif self.n_trades <= self.next_action < 2 * self.n_trades:

                client_id = self.gen_order_id()
                self.sell_id[self.next_action - self.n_trades] = client_id
                self.next_action += 1
                logger.info("sending sell order {}".format(self.sell_id))
                self.exchange.send_orders([
                    OrderCommon(symbol=self.symbol,
                                type=OrderType.Market,
                                client_id=client_id,
                                signed_qty=-self.qty)
                ])
            else:
                time.sleep(.3)
                logger.info(
                    "checking position, it should be = initial position ...")
                pos = self.exchange.get_position(self.symbol)
                assertEqual(pos.signed_qty, self.initial_pos)
                self.n_closed_positions += 1

                pnls = self.exchange.get_pnl_history(self.symbol)
                if len(pnls) != self.n_closed_positions:
                    raise AttributeError(
                        "Expected to have {} closed position, got {}".format(
                            self.n_closed_positions, len(pnls)))

                self.n_positions -= 1
                if self.n_positions > 0:
                    self.__init__(self.n_trades, self.n_positions)
                    self.handle_1m_candles('skip_candle')

        pass
Beispiel #19
0
    def _execute_order(self, order: OrderCommon) -> Fill:
        assert self.tick_num < len(self.ohlcv)

        if order.status != OrderStatus.Pending and not order.is_open():
            raise ValueError("expected order to be opened, but got " + str(order.status) + ". Order = \n"
                             + order.get_header() + "\n" + str(order))
        current_candle = self.current_candle()
        current_time = current_candle.name  # pd.Timestamp
        high = current_candle.high
        low = current_candle.low
        open = current_candle.open
        close = current_candle.close

        position = self.positions[order.symbol]  # type: PositionSim
        current_qty = position.signed_qty
        qty_fill = qty_to_close = outstanding_qty = None
        crossed = False

        if order.type is OrderType.Market:
            crossed = True
            price_fill = self._estimate_price()
            qty_fill = order.signed_qty
        elif order.type is OrderType.Limit:
            price_fill = order.price
            max_qty_fill = order.leaves_qty * order.side()
            # clip fill
            if (open <= order.price <= close) or (close <= order.price <= open):
                qty_fill = max_qty_fill
            elif high == low == order.price:
                qty_fill = round(0.5 * max_qty_fill)
            else:
                if low < order.price < high:
                    if order.is_sell():
                        factor = max((high - order.price) / (high - low), 0.50001)
                        assert factor >= 0
                    else:
                        factor = max((low - order.price) / (low - high), 0.50001)
                        assert factor >= 0
                    qty_fill = round(factor * max_qty_fill)
            if qty_fill is not None:
                crossed = True
        else:
            raise ValueError("order type " + str(order.type) + " not supported")

        if not crossed:
            return None  # type: Fill

        if position.is_open and position.would_change_side(qty_fill):
            qty_to_close = float(sign(qty_fill)) * min(abs(current_qty), abs(qty_fill))
            outstanding_qty = qty_fill - qty_to_close

        if order.fill(qty_fill) or order.type == OrderType.Market:
            order.status = OrderStatus.Filled
            order.fill_price = price_fill

        if order.price is not None and ((open <= order.price <= close) or (close <= order.price <= open)):
            assert order.status == OrderStatus.Filled

        fee = self.FEE[order.type]

        if outstanding_qty:
            position = self._update_position(order.symbol,
                                             qty=qty_to_close,
                                             price=price_fill,
                                             leverage=self.leverage[order.symbol],
                                             current_timestamp=current_time,
                                             fee=fee)
            assert not position.is_open

            position = self._update_position(order.symbol,
                                             qty=outstanding_qty,
                                             price=price_fill,
                                             leverage=self.leverage[order.symbol],
                                             current_timestamp=current_time,
                                             fee=fee)
            assert position.is_open
        else:
            self._update_position(order.symbol,
                                  qty=qty_fill,
                                  price=price_fill,
                                  leverage=self.leverage[order.symbol],
                                  current_timestamp=current_time,
                                  fee=fee)

        fill = Fill(order=order,
                    qty_filled=qty_fill,
                    price_fill=price_fill,
                    fill_time=current_time,
                    fill_type=FillType.complete if (order.status == OrderStatus.Filled) else FillType.partial)
        self.fills_hist += [fill]
        self.active_orders = drop_closed_orders_dict(self.active_orders)
        if self.can_call_handles:
            order.tactic.handle_fill(fill)
        return fill
Beispiel #20
0
 def _log_order(self, order: OrderCommon):
     self.orders_file.write(order.to_line() + '\n')