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)
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))
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) ])
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) ]
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)
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)
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) ])
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
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']) }
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
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
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
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