def handle_order_execution(self, order: Order, intrabar: Bar): amount = order.amount - order.executed_amount order.executed_amount = order.amount fee = self.taker_fee if order.limit_price: price = order.limit_price fee = self.maker_fee elif order.stop_price: price = int( order.stop_price * (1 + math.copysign(self.market_slipage_percent, order.amount) / 100) / self.symbol.tickSize) * self.symbol.tickSize else: price = intrabar.open * ( 1 + math.copysign(self.market_slipage_percent, order.amount) / 100) price = min(intrabar.high, max( intrabar.low, price)) # only prices within the bar. might mean less slipage order.executed_price = price self.account.open_position.quantity += amount delta = amount * (price if not self.symbol.isInverse else -1 / price) self.account.open_position.walletBalance -= delta self.account.open_position.walletBalance -= math.fabs(delta) * fee order.active = False order.execution_tstamp = intrabar.tstamp order.final_reason = 'executed' self.account.order_history.append(order) self.account.open_orders.remove(order) logger.debug("executed order " + order.id + " | " + str(self.account.usd_equity) + " " + str(self.account.open_position.quantity))
def internal_send_order(self, order: Order): if order.limit_price is not None: order.limit_price = round(order.limit_price, self.symbol_object.pricePrecision) if order.stop_price is not None: order.stop_price = round(order.stop_price, self.symbol_object.pricePrecision) order_type = OrderType.STOP else: order_type = OrderType.LIMIT elif order.stop_price is not None: order.stop_price = round(order.stop_price, self.symbol_object.pricePrecision) order_type = OrderType.STOP_MARKET else: order_type = OrderType.MARKET order.amount = round(order.amount, self.symbol_object.quantityPrecision) quantityFormat = "{:." + str(self.symbol_object.quantityPrecision) + "f}" priceFormat = "{:." + str(self.symbol_object.pricePrecision) + "f}" # yes have to send the price and quantity in as str (although it wants float) cause otherwise it converts it # inernally and that sometimes f**k up the precision (0.023 -> 0.02299999999) resultOrder: binance_f.model.Order = self.client.post_order( symbol=self.symbol, side=OrderSide.BUY if order.amount > 0 else OrderSide.SELL, ordertype=order_type, timeInForce=TimeInForce.GTC if order_type in [OrderType.LIMIT, OrderType.STOP] else None, quantity=quantityFormat.format(abs(order.amount)), price=priceFormat.format(order.limit_price) if order.limit_price is not None else None, stopPrice=priceFormat.format(order.stop_price) if order.stop_price is not None else None, newClientOrderId=order.id) order.exchange_id = resultOrder.orderId
def internal_send_order(self, order: Order): order_type = "Market" if order.stop_price is not None and ( self.last - order.stop_price) * order.amount >= 0: order.stop_price = None # already triggered if order.limit_price is not None: if order.stop_price is not None: order_type = "StopLimit" else: order_type = "Limit" elif order.stop_price is not None: order_type = "Stop" if (order.stop_price >= self.last and order.amount < 0) or \ (order.stop_price <= self.last and order.amount > 0): # prevent error of "would trigger immediatly" order_type = "Market" params = dict(symbol=self.symbol, clOrdID=order.id, side="Buy" if order.amount > 0 else "Sell", orderQty=abs(order.amount), ordType=order_type, stopPxEp=self.scale_price(order.stop_price), priceEp=self.scale_price(order.limit_price), triggerType="ByLastPrice" if order.stop_price is not None else None) result = self.client.place_order(params) if "data" in result.keys() and "orderID" in result["data"]["orderID"]: order.exchange_id = result["data"]["orderID"]
def internal_send_order(self, order: Order): order_type = "Market" if order.limit_price is not None: order_type = "Limit" result = None if order.stop_price is not None: # conditional order base_side = 1 if order.amount < 0 else -1 # buy stops are triggered when price goes higher (so it is # considered lower before) result = self._execute( self.bybit.Conditional.Conditional_new( side=("Buy" if order.amount > 0 else "Sell"), symbol=self.symbol, order_type=order_type, qty=abs(order.amount), price=order.limit_price, stop_px=order.stop_price, order_link_id=order.id, base_price=order.stop_price + base_side, time_in_force="GoodTillCancel")) if result is not None: order.exchange_id = result['stop_order_id'] else: result = self._execute( self.bybit.Order.Order_newV2( side=("Buy" if order.amount > 0 else "Sell"), symbol=self.symbol, order_type=order_type, qty=abs(order.amount), price=order.limit_price, order_link_id=order.id, time_in_force="GoodTillCancel")) if result is not None: order.exchange_id = result['order_id']
def update_order(self, order: Order): for existing_order in self.account.open_orders: if existing_order.id == order.id: self.account.open_orders.remove(existing_order) self.account.open_orders.append(order) order.tstamp = self.current_bars[0].last_tick_tstamp self.logger.debug("updated order %s" % (order.print_info())) break
def handle_order_execution(self, order: Order, intrabar: Bar): amount = order.amount - order.executed_amount order.executed_amount = order.amount fee = self.taker_fee if order.limit_price: price = order.limit_price fee = self.maker_fee elif order.stop_price: price = int( order.stop_price * (1 + math.copysign(self.market_slipage_percent, order.amount) / 100) / self.symbol.tickSize) * self.symbol.tickSize else: price = intrabar.open * ( 1 + math.copysign(self.market_slipage_percent, order.amount) / 100) price = min(intrabar.high, max( intrabar.low, price)) # only prices within the bar. might mean less slipage order.executed_price = price oldAmount = self.account.open_position.quantity if oldAmount != 0: oldavgentry = self.account.open_position.avgEntryPrice if oldAmount * amount > 0: self.account.open_position.avgEntryPrice = ( oldavgentry * oldAmount + price * amount) / (oldAmount + amount) if oldAmount * amount < 0: if abs(oldAmount) < abs(amount): profit = oldAmount * ( (price - oldavgentry) if not self.symbol.isInverse else (-1 / price + 1 / oldavgentry)) self.account.open_position.walletBalance += profit #close current, open new self.account.open_position.avgEntryPrice = price else: #closes the position by "-amount" cause amount is the side and direction of the close profit = -amount * ( (price - oldavgentry) if not self.symbol.isInverse else (-1 / price + 1 / oldavgentry)) self.account.open_position.walletBalance += profit else: self.account.open_position.avgEntryPrice = price self.account.open_position.quantity += amount volume = amount * (price if not self.symbol.isInverse else -1 / price) self.account.open_position.walletBalance -= math.fabs(volume) * fee order.active = False order.execution_tstamp = intrabar.tstamp order.final_reason = 'executed' self.account.order_history.append(order) self.account.open_orders.remove(order) self.logger.debug("executed order %s | %.0f %.2f | %.2f@ %.1f" % (order.id, self.account.usd_equity, self.account.open_position.quantity, order.executed_amount, order.executed_price))
def send_order(self, order: Order): # check if order is val if order.amount == 0: self.logger.error("trying to send order without amount") return self.logger.debug("added order %s" % (order.print_info())) order.tstamp = self.current_bars[0].tstamp if order not in self.account.open_orders: # bot might add it himself temporarily. self.account.open_orders.append(order)
def send_order(self, order: Order): if order.amount == 0: self.logger.error("trying to send order without amount") return if self.telegram_bot is not None: self.telegram_bot.send_log( "Sending (" + self.id + "): " + order.print_info(), order.id) order.tstamp = self.bars[0].tstamp if order not in self.account.open_orders: # bot might add it himself temporarily. self.account.open_orders.append(order) self.exchange.send_order(order)
def orderDictToOrder(self, o) -> Order: """ { "bizError": 0, "orderID": "9cb95282-7840-42d6-9768-ab8901385a67", "clOrdID": "7eaa9987-928c-652e-cc6a-82fc35641706", "symbol": "BTCUSD", "side": "Buy", "actionTimeNs": 1580533011677666800, "transactTimeNs": 1580533011677666800, "orderType": null, "priceEp": 84000000, "price": 8400, "orderQty": 1, "displayQty": 1, "timeInForce": null, "reduceOnly": false, "stopPxEp": 0, "closedPnlEv": 0, "closedPnl": 0, "closedSize": 0, "cumQty": 0, "cumValueEv": 0, "cumValue": 0, "leavesQty": 0, "leavesValueEv": 0, "leavesValue": 0, "stopPx": 0, "stopDirection": "Falling", "ordStatus": "Untriggered" }, """ sideMult = -1 if o['side'] == Client.SIDE_SELL else 1 stop = self.noneIfZero( o['stopPx']) if 'stopPx' in o else self.noneIfZero( o['stopPxEp'], True) price = self.noneIfZero( o['price']) if 'price' in o else self.noneIfZero( o['priceEp'], True) order = Order(orderId=o['clOrdID'], stop=stop, limit=price, amount=o['orderQty'] * sideMult) order.exchange_id = o['orderID'] order.tstamp = o['actionTimeNs'] / 1000000000 order.active = o['ordStatus'] in [ Client.ORDER_STATUS_NEW, Client.ORDER_STATUS_UNTRIGGERED, Client.ORDER_STATUS_TRIGGERED ] order.executed_amount = o['cumQty'] * sideMult val = o['cumValue'] if 'cumValue' in o else o[ 'cumValueEv'] / self.valueScale order.executed_price = o['cumQty'] / val if val != 0 else 0 if order.executed_amount != 0: order.execution_tstamp = o['transactTimeNs'] / 1000000000 order.stop_triggered = order.stop_price is not None and o[ 'ordStatus'] == Client.ORDER_STATUS_TRIGGERED return order
def internal_send_order(self, order: Order): order_type = "Market" if order.limit_price is not None: order_type = "Limit" if order.stop_price is not None and (self.last - order.stop_price) * order.amount >= 0: order.stop_price = None # already triggered orderType = TradingBot.order_type_from_order_id(order.id) if order.stop_price is not None: # conditional order base_side = self.symbol_info.tickSize * ( 1 if order.amount < 0 else -1) # buy stops are triggered when price goes higher (so it is # considered lower before) normalizedStop = self.symbol_info.normalizePrice(order.stop_price, order.amount > 0) result = self._execute(self.bybit.LinearConditional.LinearConditional_new(side=("Buy" if order.amount > 0 else "Sell"), symbol=self.symbol, order_type=order_type, qty=strOrNone( self.symbol_info.normalizeSize( abs(order.amount))), price=strOrNone( self.symbol_info.normalizePrice( order.limit_price, order.amount < 0)), stop_px=strOrNone(normalizedStop), order_link_id=order.id, base_price=strOrNone(round( normalizedStop + base_side, self.symbol_info.pricePrecision)), time_in_force="GoodTillCancel", reduce_only= orderType != OrderType.ENTRY, close_on_trigger= orderType != OrderType.ENTRY)) if result is not None: order.exchange_id = result['stop_order_id'] else: result = self._execute(self.bybit.LinearOrder.LinearOrder_new(side=("Buy" if order.amount > 0 else "Sell"), symbol=self.symbol, order_type=order_type, qty=strOrNone( self.symbol_info.normalizeSize( abs(order.amount))), price=strOrNone( self.symbol_info.normalizePrice(order.limit_price, order.amount < 0)), order_link_id=order.id, time_in_force="GoodTillCancel", reduce_only= orderType != OrderType.ENTRY, close_on_trigger= orderType != OrderType.ENTRY)) if result is not None: order.exchange_id = result['order_id']
def update_order(self, order: Order): for existing_order in self.account.open_orders: if existing_order.id == order.id: self.account.open_orders.remove(existing_order) self.account.open_orders.append(order) self.logger.debug("updated order %s" % (order.print_info())) break
def internal_send_order(self, order: Order): order_type = "Market" if order.limit_price is not None: order_type = "Limit" if order.stop_price is not None and ( self.last - order.stop_price) * order.amount >= 0: order.stop_price = None # already triggered if order.stop_price is not None: # conditional order base_side = self.symbol_info.tickSize * ( 1 if order.amount < 0 else -1 ) # buy stops are triggered when price goes higher (so it is # considered lower before) normalizedStop = self.symbol_info.normalizePrice( order.stop_price, order.amount > 0) result = self.handle_result( lambda: self.pybit.place_conditional_order( side=("Buy" if order.amount > 0 else "Sell"), symbol=self.symbol, order_type=order_type, qty=strOrNone(int(abs(order.amount))), price=strOrNone( self.symbol_info.normalizePrice( order.limit_price, order.amount < 0)), stop_px=strOrNone(normalizedStop), order_link_id=order.id, base_price=strOrNone( round(normalizedStop + base_side, self.symbol_info. pricePrecision)), time_in_force="GoodTillCancel")) if result is not None: order.exchange_id = result['stop_order_id'] else: result = self.handle_result(lambda: self.pybit.place_active_order( side=("Buy" if order.amount > 0 else "Sell"), symbol=self.symbol, order_type=order_type, qty=strOrNone(int(abs(order.amount))), price=strOrNone( self.symbol_info.normalizePrice(order.limit_price, order. amount < 0)), order_link_id=order.id, time_in_force="GoodTillCancel")) if result is not None: order.exchange_id = result['order_id']
def update_order(self, order: Order): if self.telegram_bot is not None: self.telegram_bot.send_log( "updating (" + self.id + "): " + order.print_info(), order.id) self.exchange.update_order(order) self.exchange.on_tick_callback( True ) ##simulate tick to prevent early updates (need to wait for exchange to update order
def position_got_opened_or_changed(self, position: Position, bars: List[Bar], account: Account, open_positions): other_id = TradingBot.get_other_direction_id(position.id) if other_id in open_positions.keys(): open_positions[other_id].markForCancel = bars[0].tstamp # add stop gotStop = False # safety check needed to not add multiple SL in case of an error gotTp = False for order in account.open_orders: orderType = TradingBot.order_type_from_order_id(order.id) posId = TradingBot.position_id_from_order_id(order.id) if orderType == OrderType.SL and posId == position.id: gotStop = True if abs(order.amount + position.current_open_amount) > self.symbol.lotSize / 2: order.amount = -position.current_open_amount self.order_interface.update_order(order) elif self.tp_fac > 0 and orderType == OrderType.TP and posId == position.id: gotTp = True amount = self.symbol.normalizeSize( -position.current_open_amount + order.executed_amount) if abs(order.amount - amount) > self.symbol.lotSize / 2: order.amount = amount self.order_interface.update_order(order) if not gotStop: order = Order(orderId=TradingBot.generate_order_id( positionId=position.id, type=OrderType.SL), stop=position.initial_stop, amount=-position.amount) self.order_interface.send_order(order) if self.tp_fac > 0 and not gotTp: ref = position.filled_entry - position.initial_stop #tp = position.filled_entry + ref * self.tp_fac data: Data = self.channel.get_data(bars[1]) if order.amount < 0: tp = data.shortTrail else: tp = data.longTrail order = Order(orderId=TradingBot.generate_order_id( positionId=position.id, type=OrderType.TP), limit=tp, amount=-position.amount)
def send_order(self, order: Order): if order.amount == 0: self.logger.error("trying to send order without amount") return order.tstamp = self.bars[0].tstamp if order not in self.account.open_orders: # bot might add it himself temporarily. self.account.open_orders.append(order) self.exchange.send_order(order)
def manage_open_position(self, position, bars, account, pos_ids_to_cancel): if self.close_after_bars >= 0 \ and position.status == PositionStatus.OPEN \ and position.entry_tstamp < bars[self.close_after_bars].tstamp: self.order_interface.send_order( Order(orderId=TradingBot.generate_order_id( positionId=position.id, type=OrderType.SL), amount=-position.currentOpenAmount))
def position_got_opened(self, position: Position, bars: List[Bar], account: Account, open_positions): gotTP = False gotSL = False for order in position.connectedOrders: type = TradingBot.order_type_from_order_id(order.id) if type == OrderType.TP: gotTP = True amount = self.symbol.normalizeSize( -position.currentOpenAmount + order.executed_amount) if abs(order.amount - amount) > self.symbol.lotSize / 2: order.amount = amount self.order_interface.update_order(order) if type == OrderType.SL: gotSL = True amount = self.symbol.normalizeSize(-position.currentOpenAmount) if abs(order.amount - amount) > self.symbol.lotSize / 2: order.amount = amount self.order_interface.update_order(order) if not gotTP: slDiff = position.wanted_entry - position.initial_stop # reverse calc the std at time of signal and use tp factor accordingly tp = self.symbol.normalizePrice( position.wanted_entry + slDiff / (self.sl_factor - self.entry_factor) * (self.entry_factor - self.tp_factor), position.amount > 0) self.order_interface.send_order( Order(orderId=TradingBot.generate_order_id( position.id, OrderType.TP), amount=-position.currentOpenAmount, stop=None, limit=tp)) if not gotSL: order = Order(orderId=TradingBot.generate_order_id( positionId=position.id, type=OrderType.SL), stop=position.initial_stop, amount=-position.currentOpenAmount) self.order_interface.send_order(order) # added temporarily, cause sync with open orders is in the next loop and otherwise the orders vs # position check fails if order not in account.open_orders: # outside world might have already added it account.open_orders.append(order)
def send_order(self, order: Order): # check if order is val if order.amount == 0: self.logger.error("trying to send order without amount") return [posId, order_type] = TradingBot.position_id_and_type_from_order_id(order.id) if order_type == OrderType.ENTRY: [unused, direction] = TradingBot.split_pos_Id(posId) if direction == PositionDirection.LONG and order.amount < 0: self.logger.error("sending long entry with negative amount") if direction == PositionDirection.SHORT and order.amount > 0: self.logger.error("sending short entry with positive amount") self.logger.debug("added order %s" % (order.print_info())) order.tstamp = self.current_bars[0].tstamp if order not in self.account.open_orders: # bot might add it himself temporarily. self.account.open_orders.append(order)
def get_orders(self) -> List[Order]: mexOrders = self.bitmex.open_orders() result: List[Order] = [] for o in mexOrders: sideMulti = 1 if o["side"] == "Buy" else -1 order = Order(orderId=o["clOrdID"], stop=o["stopPx"], limit=o["price"], amount=o["orderQty"] * sideMulti) order.stop_triggered = o["triggered"] == "StopOrderTriggered" order.executed_amount = (o["cumQty"]) * sideMulti order.tstamp = parse_utc_timestamp(o['timestamp']) order.execution_tstamp = order.tstamp order.active = o['ordStatus'] == 'New' order.exchange_id = o["orderID"] order.executed_price = o["avgPx"] result.append(order) return result
def position_got_opened(self, position: Position, bars: List[Bar], account: Account, open_positions): other_id = TradingBot.get_other_direction_id(position.id) if other_id in open_positions.keys(): open_positions[other_id].markForCancel = bars[0].tstamp # add stop order = Order(orderId=TradingBot.generate_order_id(positionId=position.id, type=OrderType.SL), stop=position.initial_stop, amount=-position.amount) self.order_interface.send_order(order) # added temporarily, cause sync with open orders is in the next loop and otherwise the orders vs # position check fails if order not in account.open_orders: # outside world might have already added it account.open_orders.append(order)
def convertOrder(self, apiOrder: binance_f.model.Order) -> Order: direction = 1 if apiOrder.side == OrderSide.BUY else -1 order = Order(orderId=apiOrder.clientOrderId, amount=apiOrder.origQty * direction, limit=apiOrder.price, stop=apiOrder.stopPrice) order.executed_amount = apiOrder.executedQty * direction order.executed_price = apiOrder.avgPrice order.active = apiOrder.status in ["NEW", "PARTIALLY_FILLED"] order.exchange_id = apiOrder.orderId return order
def internal_send_order(self, order: Order): if order.limit_price is not None: if order.stop_price is not None: order_type = OrderType.STOP else: order_type = OrderType.LIMIT else: order_type = OrderType.STOP_MARKET resultOrder: binance_f.model.Order = self.client.post_order( symbol=self.symbol, side=OrderSide.BUY if order.amount > 0 else OrderSide.SELL, ordertype=order_type, timeInForce=TimeInForce.GTC, quantity=abs(order.amount), price=order.limit_price, stopPrice=order.stop_price, newClientOrderId=order.id) order.exchange_id = resultOrder.orderId
def position_got_opened(self, position: Position, bars: List[Bar], account: Account, open_positions): other_id = TradingBot.get_other_direction_id(position.id) if other_id in open_positions.keys(): open_positions[other_id].markForCancel = bars[0].tstamp # add stop gotStop= False # safety check needed to not add multiple SL in case of an error for order in account.open_orders: orderType = TradingBot.order_type_from_order_id(order.id) posId = TradingBot.position_id_from_order_id(order.id) if orderType == OrderType.SL and posId == position.id: gotStop= True break if not gotStop: order = Order(orderId=TradingBot.generate_order_id(positionId=position.id, type=OrderType.SL), stop=position.initial_stop, amount=-position.amount) self.order_interface.send_order(order) # added temporarily, cause sync with open orders is in the next loop and otherwise the orders vs # position check fails if order not in account.open_orders: # outside world might have already added it account.open_orders.append(order)
def cancel_order(self, order: Order): if self.telegram_bot is not None: self.telegram_bot.send_log( "canceling (" + self.id + "): " + order.print_info(), order.id) order.active = False # already mark it as cancelled, so not to mess up next loop self.exchange.cancel_order(order)
def __open_position(self, direction, bars, swing, open_positions): directionFactor = 1 oppDirection = PositionDirection.SHORT extreme = bars[1].low capFunc = min if direction == PositionDirection.SHORT: directionFactor = -1 oppDirection = PositionDirection.LONG extreme = bars[1].high capFunc = max oppDirectionFactor = directionFactor * -1 expectedEntrySplipagePerc = 0.0015 expectedExitSlipagePerc = 0.0015 data: Data = self.channel.get_data(bars[1]) if self.close_on_opposite: for pos in open_positions.values(): if pos.status == PositionStatus.OPEN and \ TradingBot.split_pos_Id(pos.id)[1] == oppDirection: # execution will trigger close and cancel of other orders self.order_interface.send_order( Order(orderId=TradingBot.generate_order_id( pos.id, OrderType.SL), amount=-pos.amount, stop=None, limit=None)) if self.init_stop_type == 1: stop = extreme elif self.init_stop_type == 2: stop = extreme + (extreme - bars[1].close) * 0.5 else: stop = capFunc(swing, (extreme + bars[1].close) / 2) stop = stop + oppDirectionFactor # buffer entry = bars[0].open signalId = self.get_signal_id(bars) #case long: entry * (1 + oppDirectionFactor*self.min_stop_diff_perc / 100) >= stop if 0 <= directionFactor*(entry * (1 + oppDirectionFactor*self.min_stop_diff_perc / 100) - stop) \ or not self.ignore_on_tight_stop: stop = capFunc( stop, entry * (1 + oppDirectionFactor * self.min_stop_diff_perc / 100)) amount = self.calc_pos_size( risk=self.risk_factor, exitPrice=stop * (1 + oppDirectionFactor * expectedExitSlipagePerc), entry=entry * (1 + directionFactor * expectedEntrySplipagePerc), atr=data.atr) posId = TradingBot.full_pos_id(signalId, direction) pos = Position(id=posId, entry=entry, amount=amount, stop=stop, tstamp=bars[0].tstamp) open_positions[posId] = pos self.order_interface.send_order( Order(orderId=TradingBot.generate_order_id( posId, OrderType.ENTRY), amount=amount, stop=None, limit=None)) self.order_interface.send_order( Order(orderId=TradingBot.generate_order_id( posId, OrderType.SL), amount=-amount, stop=stop, limit=None)) if self.tp_fac > 0: ref = entry - stop if self.tp_use_atr: ref = math.copysign(data.atr, entry - stop) tp = entry + ref * self.tp_fac self.order_interface.send_order( Order(orderId=TradingBot.generate_order_id( posId, OrderType.TP), amount=-amount, stop=None, limit=tp)) pos.status = PositionStatus.OPEN
def orderDictToOrder(o): sideMulti = 1 if o["side"] == "Buy" else -1 ext = o['ext_fields'] if 'ext_fields' in o.keys() else None stop = o['trigger_price'] if 'trigger_price' in o.keys() else None if stop is None: stop = o['stop_px'] if 'stop_px' in o.keys() else None if stop is None and ext is not None and 'trigger_price' in ext.keys(): stop = ext['trigger_price'] order = Order( orderId=o["order_link_id"], stop=float(stop) if stop is not None else None, limit=float(o["price"]) if o['order_type'] == 'Limit' else None, amount=float(o["qty"] * sideMulti)) if "order_status" in o.keys(): order.stop_triggered = o[ "order_status"] == "New" and stop is not None order.active = o['order_status'] == 'New' or o[ 'order_status'] == 'Untriggered' elif "stop_order_status" in o.keys(): order.stop_triggered = o["stop_order_status"] == 'Triggered' or o[ 'stop_order_status'] == 'Active' order.active = o['stop_order_status'] == 'Triggered' or o[ 'stop_order_status'] == 'Untriggered' exec = o['cum_exec_qty'] if 'cum_exec_qty' in o.keys() else 0 order.executed_amount = float(exec) * sideMulti order.tstamp = parse_utc_timestamp(o['timestamp'] if 'timestamp' in o.keys() else o['created_at']) order.exchange_id = o["order_id"] if 'order_id' in o.keys( ) else o['stop_order_id'] order.executed_price = None if 'cum_exec_value' in o.keys() and 'cum_exec_qty' in o.keys( ) and float(o['cum_exec_value']) != 0: order.executed_price = o['cum_exec_qty'] / float( o["cum_exec_value"]) # cause of inverse return order
def sync_positions_with_open_orders(self, bars: List[Bar], account: Account): open_pos = 0 for pos in self.open_positions.values(): pos.connectedOrders= [] # will be filled now if pos.status == PositionStatus.OPEN: open_pos += pos.amount if not self.got_data_for_position_sync(bars): self.logger.warn("got no initial data, can't sync positions") return remaining_pos_ids = [] remaining_pos_ids += self.open_positions.keys() remaining_orders = [] remaining_orders += account.open_orders # first check if there even is a diparity (positions without stops, or orders without position) for order in account.open_orders: if not order.active: remaining_orders.remove(order) continue # got cancelled during run orderType = self.order_type_from_order_id(order.id) if orderType is None: remaining_orders.remove(order) continue # none of ours posId = self.position_id_from_order_id(order.id) if posId in self.open_positions.keys(): pos = self.open_positions[posId] pos.connectedOrders.append(order) remaining_orders.remove(order) if posId in remaining_pos_ids: if (orderType == OrderType.SL and pos.status == PositionStatus.OPEN) \ or (orderType == OrderType.ENTRY and pos.status == PositionStatus.PENDING): # only remove from remaining if its open with SL or pending with entry. every position needs # a stoploss! remaining_pos_ids.remove(posId) for pos in self.open_positions.values(): self.check_open_orders_in_position(pos) if len(remaining_orders) == 0 and len(remaining_pos_ids) == 0 and abs( open_pos - account.open_position.quantity) < 0.1: return self.logger.info("Has to start order/pos sync with bot vs acc: %.3f vs. %.3f and %i vs %i, remaining: %i, %i" % ( open_pos, account.open_position.quantity, len(self.open_positions), len(account.open_orders), len(remaining_orders), len(remaining_pos_ids))) remainingPosition = account.open_position.quantity for pos in self.open_positions.values(): if pos.status == PositionStatus.OPEN: remainingPosition -= pos.amount waiting_tps = [] # now remaining orders and remaining positions contain the not matched ones for order in remaining_orders: orderType = self.order_type_from_order_id(order.id) posId = self.position_id_from_order_id(order.id) if not order.active: # already canceled or executed continue if orderType == OrderType.ENTRY: # add position for unkown order stop = self.get_stop_for_unmatched_amount(order.amount, bars) if stop is not None: newPos = Position(id=posId, entry=order.limit_price if order.limit_price is not None else order.stop_price, amount=order.amount, stop=stop, tstamp=bars[0].tstamp) newPos.status = PositionStatus.PENDING if not order.stop_triggered else PositionStatus.TRIGGERED self.open_positions[posId] = newPos self.logger.warn("found unknown entry %s %.1f @ %.1f, added position" % (order.id, order.amount, order.stop_price if order.stop_price is not None else order.limit_price)) else: self.logger.warn( "found unknown entry %s %.1f @ %.1f, but don't know what stop to use -> canceling" % (order.id, order.amount, order.stop_price if order.stop_price is not None else order.limit_price)) self.order_interface.cancel_order(order) elif orderType == OrderType.SL and remainingPosition * order.amount < 0 and abs(remainingPosition) > abs( order.amount): # only assume open position for the waiting SL with the remainingPosition also indicates it, # otherwise it might be a pending cancel (from executed TP) or already executed newPos = Position(id=posId, entry=None, amount=-order.amount, stop=order.stop_price, tstamp=bars[0].tstamp) newPos.status = PositionStatus.OPEN remainingPosition -= newPos.amount self.open_positions[posId] = newPos self.logger.warn("found unknown exit %s %.1f @ %.1f, opened position for it" % ( order.id, order.amount, order.stop_price if order.stop_price is not None else order.limit_price)) else: waiting_tps.append(order) # cancel orphaned TPs for order in waiting_tps: orderType = self.order_type_from_order_id(order.id) posId = self.position_id_from_order_id(order.id) if posId not in self.open_positions.keys(): # still not in (might have been added in previous for) self.logger.warn( "didn't find matching position for order %s %.1f @ %.1f -> canceling" % (order.id, order.amount, order.stop_price if order.stop_price is not None else order.limit_price)) self.order_interface.cancel_order(order) self.logger.info("found " + str(len(self.open_positions)) + " existing positions on sync") # positions with no exit in the market for posId in remaining_pos_ids: pos = self.open_positions[posId] if pos.status == PositionStatus.PENDING or pos.status == PositionStatus.TRIGGERED: # should have the opening order in the system, but doesn't # not sure why: in doubt: not create wrong orders if remainingPosition * pos.amount > 0 and abs(remainingPosition) >= abs(pos.amount): # assume position was opened without us realizing (during downtime) self.logger.warn( "pending position with no entry order but open position looks like it was opened: %s" % (posId)) self.handle_opened_position(position=pos, order=None, bars=bars, account=account) remainingPosition -= pos.amount else: self.logger.warn( "pending position with no entry order and no sign of opening -> close missed: %s" % (posId)) pos.status = PositionStatus.MISSED self.position_closed(pos, account) elif pos.status == PositionStatus.OPEN: if remainingPosition == 0 and pos.initial_stop is not None: # for some reason everything matches but we are missing the stop in the market self.logger.warn( "found position with no stop in market. added stop for it: %s with %.1f contracts" % ( posId, pos.amount)) self.order_interface.send_order( Order(orderId=self.generate_order_id(posId, OrderType.SL), amount=-pos.amount, stop=pos.initial_stop)) else: self.logger.warn( "found position with no stop in market. %s with %.1f contracts. but remaining Position doesn't match so assume it was already closed." % ( posId, pos.amount)) self.position_closed(pos, account) remainingPosition += pos.amount else: self.logger.warn( "pending position with noconnection order not pending or open? closed: %s" % (posId)) self.position_closed(pos, account) # now there should not be any mismatch between positions and orders. if remainingPosition != 0: unmatched_stop = self.get_stop_for_unmatched_amount(remainingPosition, bars) signalId = str(bars[1].tstamp) + '+' + str(randint(0, 99)) if unmatched_stop is not None: posId = self.full_pos_id(signalId, PositionDirection.LONG if remainingPosition > 0 else PositionDirection.SHORT) newPos = Position(id=posId, entry=None, amount=remainingPosition, stop=unmatched_stop, tstamp=bars[0].tstamp) newPos.status = PositionStatus.OPEN self.open_positions[posId] = newPos # add stop self.logger.info( "couldn't account for " + str(newPos.amount) + " open contracts. Adding position with stop for it") self.order_interface.send_order(Order(orderId=self.generate_order_id(posId, OrderType.SL), stop=newPos.initial_stop, amount=-newPos.amount)) elif account.open_position.quantity * remainingPosition > 0: self.logger.info( "couldn't account for " + str(remainingPosition) + " open contracts. Market close") self.order_interface.send_order(Order(orderId=signalId + "_marketClose", amount=-remainingPosition)) else: self.logger.info( "couldn't account for " + str( remainingPosition) + " open contracts. But close would increase exposure-> ignored")
def run(self): self.reset() self.logger.info("starting backtest with " + str(len(self.bars)) + " bars and " + str(self.account.equity) + " equity") for i in range(self.bot.min_bars_needed(), len(self.bars)): if i == len(self.bars) - 1 or i < self.bot.min_bars_needed(): continue # ignore last bar and first x # slice bars. TODO: also slice intrabar to simulate tick self.current_bars = self.bars[-(i + 1):] # add one bar with 1 tick on open to show to bot that the old one is closed next_bar = self.bars[-i - 2] forming_bar = Bar(tstamp=next_bar.tstamp, open=next_bar.open, high=next_bar.open, low=next_bar.open, close=next_bar.open, volume=0, subbars=[]) self.current_bars.insert(0, forming_bar) self.current_bars[0].did_change = True self.current_bars[1].did_change = True self.do_funding() # self.bot.on_tick(self.current_bars, self.account) for subbar in reversed(next_bar.subbars): # check open orders & update account self.handle_open_orders(subbar) open = len(self.account.open_orders) forming_bar.add_subbar(subbar) self.bot.on_tick(self.current_bars, self.account) if open != len(self.account.open_orders): self.handle_open_orders(subbar) # got new ones self.current_bars[1].did_change = False next_bar.bot_data = forming_bar.bot_data for b in self.current_bars: if b.did_change: b.did_change = False else: break # no need to go further if self.account.open_position.quantity != 0: self.send_order( Order(orderId="endOfTest", amount=-self.account.open_position.quantity)) self.handle_open_orders(self.bars[0].subbars[-1]) if len(self.bot.position_history) > 0: daysInPos = 0 maxDays = 0 minDays = self.bot.position_history[0].daysInPos() if len( self.bot.position_history) > 0 else 0 for pos in self.bot.position_history: if pos.status != PositionStatus.CLOSED: continue if pos.exit_tstamp is None: pos.exit_tstamp = self.bars[0].tstamp daysInPos += pos.daysInPos() maxDays = max(maxDays, pos.daysInPos()) minDays = min(minDays, pos.daysInPos()) daysInPos /= len(self.bot.position_history) profit = self.account.equity - self.initialEquity uw_updates_per_day = 1440 # every minute total_days = (self.bars[0].tstamp - self.bars[-1].tstamp) / (60 * 60 * 24) rel = profit / (self.maxDD if self.maxDD > 0 else 1) rel_per_year = rel / (total_days / 365) self.logger.info( "finished | closed pos: " + str(len(self.bot.position_history)) + " | open pos: " + str(len(self.bot.open_positions)) + " | profit: " + ("%.2f" % (100 * profit / self.initialEquity)) + " | HH: " + ("%.2f" % (100 * (self.hh / self.initialEquity - 1))) + " | maxDD: " + ("%.2f" % (100 * self.maxDD / self.initialEquity)) + " | maxExp: " + ("%.2f" % (self.maxExposure / self.initialEquity)) + " | rel: " + ("%.2f" % (rel_per_year)) + " | UW days: " + ("%.1f" % (self.max_underwater / uw_updates_per_day)) + " | pos days: " + ("%.1f/%.1f/%.1f" % (minDays, daysInPos, maxDays))) else: self.logger.info("finished with no trades") #self.write_results_to_files() return self
def callback(self, data_type: 'SubscribeMessageType', event: 'any'): gotTick = False # refresh userdata every 15 min if self.lastUserDataKeep < time.time() - 15 * 60: self.lastUserDataKeep = time.time() self.client.keep_user_data_stream() # TODO: implement! (update bars, orders and account) if data_type == SubscribeMessageType.RESPONSE: pass # what to do herE? elif data_type == SubscribeMessageType.PAYLOAD: if event.eventType == "kline": # {'eventType': 'kline', 'eventTime': 1587064627164, 'symbol': 'BTCUSDT', # 'data': <binance_f.model.candlestickevent.Candlestick object at 0x0000016B89856760>} if event.symbol == self.symbol: candle: Candlestick = event.data if len(self.candles) > 0: if candle.startTime <= self.candles[ 0].startTime and candle.startTime > self.candles[ -1].startTime: # somewhere inbetween to replace for idx in range(0, len(self.candles)): if candle.startTime == self.candles[ idx].startTime: self.candles[idx] = candle break elif candle.startTime > self.candles[0].startTime: self.candles.insert(0, candle) gotTick = True else: self.candles.append(candle) gotTick = True elif (event.eventType == "ACCOUNT_UPDATE"): # {'eventType': 'ACCOUNT_UPDATE', 'eventTime': 1587063874367, 'transactionTime': 1587063874365, # 'balances': [<binance_f.model.accountupdate.Balance object at 0x000001FAF470E100>,...], # 'positions': [<binance_f.model.accountupdate.Position object at 0x000001FAF470E1C0>...]} usdBalance = 0 for b in event.balances: bal: Balance = b if bal.asset == "USDT": usdBalance = bal.walletBalance for p in event.positions: pos: Position = p if pos.symbol not in self.positions.keys(): self.positions[pos.symbol] = AccountPosition( pos.symbol, avgEntryPrice=float(pos.entryPrice), quantity=float(pos.amount), walletBalance=usdBalance if "USDT" in pos.symbol else 0) else: accountPos = self.positions[pos.symbol] accountPos.quantity = float(pos.amount) accountPos.avgEntryPrice = float(pos.entryPrice) if "USDT" in pos.symbol: accountPos.walletBalance = usdBalance elif (event.eventType == "ORDER_TRADE_UPDATE"): # {'eventType': 'ORDER_TRADE_UPDATE', 'eventTime': 1587063513592, 'transactionTime': 1587063513589, # 'symbol': 'BTCUSDT', 'clientOrderId': 'web_ybDNrTjCi765K3AvOMRK', 'side': 'BUY', 'type': 'LIMIT', # 'timeInForce': 'GTC', 'origQty': 0.01, 'price': 6901.0, 'avgPrice': 0.0, 'stopPrice': 0.0, # 'executionType': 'NEW', 'orderStatus': 'NEW', 'orderId': 2705199704, 'lastFilledQty': 0.0, # 'cumulativeFilledQty': 0.0, 'lastFilledPrice': 0.0, 'commissionAsset': None, 'commissionAmount': None, # 'orderTradeTime': 1587063513589, 'tradeID': 0, 'bidsNotional': 138.81, 'asksNotional': 0.0, # 'isMarkerSide': False, 'isReduceOnly': False, 'workingType': 'CONTRACT_PRICE'} sideMulti = 1 if event.side == 'BUY' else -1 order: Order = Order(orderId=event.clientOrderId, stop=event.stopPrice, limit=event.price, amount=event.origQty * sideMulti) order.exchange_id = event.orderId #FIXME: how do i know stop triggered on binance? #order.stop_triggered = order.executed_amount = event.cumulativeFilledQty * sideMulti order.executed_price = event.avgPrice order.tstamp = event.transactionTime order.execution_tstamp = event.orderTradeTime prev: Order = self.orders[ order. exchange_id] if order.exchange_id in self.orders.keys( ) else None if prev is not None: if prev.tstamp > order.tstamp or abs( prev.executed_amount) > abs(order.executed_amount): # already got newer information, probably the info of the stop order getting # triggered, when i already got the info about execution self.logger.info("ignoring delayed update for %s " % (prev.id)) if order.stop_price is None: order.stop_price = prev.stop_price if order.limit_price is None: order.limit_price = prev.limit_price prev = order if not prev.active and prev.execution_tstamp == 0: prev.execution_tstamp = datetime.utcnow().timestamp() self.orders[order.exchange_id] = prev self.logger.info("received order update: %s" % (str(order))) else: self.logger.warn("Unknown Data in websocket callback") if gotTick and self.on_tick_callback is not None: self.on_tick_callback() # got something new
def open_orders(self, is_new_bar, directionFilter, bars, account, open_positions): if (not is_new_bar) or len(bars) < self.min_bars_needed(): return # only open orders on beginning of bar if not self.entries_allowed(bars): self.logger.info("no entries allowed") return # check for signal. we are at the open of the new bar. so bars[0] contains of only 1 tick. # we look at data bars[1] and bars[2] prevFast = self.fastMA.get_data(bars[2]) currentFast = self.fastMA.get_data(bars[1]) prevSlow = self.slowMA.get_data(bars[2]) currentSlow = self.slowMA.get_data(bars[1]) swingData: Data = self.swings.get_data(bars[1]) # for stops # include the expected slipage in the risk calculation expectedEntrySplipagePerc = 0.0015 expectedExitSlipagePerc = 0.0015 signalId = "MACross+" + str(bars[0].tstamp) if prevFast <= prevSlow and currentFast > currentSlow: # cross up -> long entry entry = bars[0].open # current price stop = swingData.swingLow if stop is None: stop = lowest(bars, self.swings.before + self.swings.after, 1, BarSeries.LOW) amount = self.calc_pos_size( risk=self.risk_factor, exitPrice=stop * (1 - expectedExitSlipagePerc), entry=entry * (1 + expectedEntrySplipagePerc)) # open the position and save it posId = TradingBot.full_pos_id(signalId, PositionDirection.LONG) pos = Position(id=posId, entry=entry, amount=amount, stop=stop, tstamp=bars[0].tstamp) open_positions[posId] = pos # send entry as market, immediatly send SL too self.order_interface.send_order( Order(orderId=TradingBot.generate_order_id( posId, OrderType.ENTRY), amount=amount, stop=None, limit=None)) self.order_interface.send_order( Order(orderId=TradingBot.generate_order_id( posId, OrderType.SL), amount=-amount, stop=stop, limit=None)) pos.status = PositionStatus.OPEN elif prevFast >= prevSlow and currentFast < currentSlow: # cross down -> short entry entry = bars[0].open # current price stop = swingData.swingHigh if stop is None: stop = highest(bars, self.swings.before + self.swings.after, 1, BarSeries.HIGH) amount = self.calc_pos_size( risk=self.risk_factor, exitPrice=stop * (1 + expectedExitSlipagePerc), entry=entry * (1 - expectedEntrySplipagePerc)) # open the position and save it posId = TradingBot.full_pos_id(signalId, PositionDirection.SHORT) pos = Position(id=posId, entry=entry, amount=amount, stop=stop, tstamp=bars[0].tstamp) open_positions[posId] = pos # send entry as market, immediatly send SL too self.order_interface.send_order( Order(orderId=TradingBot.generate_order_id( posId, OrderType.ENTRY), amount=amount, stop=None, limit=None)) self.order_interface.send_order( Order(orderId=TradingBot.generate_order_id( posId, OrderType.SL), amount=-amount, stop=stop, limit=None)) pos.status = PositionStatus.OPEN