def process_trade(self, paper, orderType, request, payload): outputTitle = None outputMessage = None exchange = request.get_exchange() ticker = request.get_ticker() isLimitOrder = request.find_parameter_in_list("isLimitOrder", request.get_filters(), default=False) isAmountPercent = request.find_parameter_in_list("isAmountPercent", request.get_filters(), default=False) isPricePercent = request.find_parameter_in_list("isPricePercent", request.get_filters(), default=False) reduceOnly = request.find_parameter_in_list( "isReduceOnlyMode", request.get_filters(), default=False) or "stop" in orderType if not isLimitOrder: execPrice = payload["quotePrice"] elif isLimitOrder and len(request.get_numerical_parameters()) != 2: outputTitle = "Execution price was not provided." outputMessage = "A limit order execution price must be provided." return outputTitle, outputMessage, paper, None else: execPrice = request.get_numerical_parameters()[1] execAmount = request.get_numerical_parameters()[0] inverseOrderType = "sell" if "buy" in orderType else "buy" if exchange.id not in paper: paper[exchange.id] = { "balance": copy.deepcopy(PaperTrader.startingBalance[exchange.id]), "openOrders": [], "history": [] } if isLimitOrder and len(paper[exchange.id]["openOrders"]) >= 1000: outputTitle = "Too many open paper orders" outputMessage = "Only up to 1000 open paper orders are allowed." return outputTitle, outputMessage, paper, None base = ticker.name if exchange.id in ["bitmex"] else ticker.base quote = "BTC" if exchange.id in ["bitmex"] else ticker.quote if base not in paper[exchange.id]["balance"]: paper[exchange.id]["balance"][base] = {"entry": 0.0, "amount": 0.0} if quote not in paper[exchange.id]["balance"]: paper[exchange.id]["balance"][quote] = { "entry": 0.0, "amount": 0.0 } baseOrder = paper[exchange.id]["balance"][base] quoteOrder = paper[exchange.id]["balance"][quote] if orderType.endswith("buy"): if isPricePercent: execPrice = payload["quotePrice"] * (1 - execPrice / 100) execAmount = ( (abs(quoteOrder["amount"]) * execPrice if exchange.id in ["bitmex"] else abs(quoteOrder["amount"]) / execPrice) * (execAmount / 100)) if isAmountPercent else execAmount elif orderType.endswith("sell"): if isPricePercent: execPrice = payload["quotePrice"] * (1 + execPrice / 100) execAmount = ( (quoteOrder["amount"] * execPrice if exchange.id in ["bitmex"] else baseOrder["amount"]) * (execAmount / 100)) if isAmountPercent else execAmount if exchange.id in ["bitmex"]: execAmount *= -1 execPriceText = Utils.format_price(exchange.properties, ticker.symbol, execPrice) execPrice = float(execPriceText.replace(",", "")) execAmountText = Utils.format_amount(exchange.properties, ticker.symbol, execAmount) execAmount = float(execAmountText.replace(",", "")) if exchange.id in ["bitmex"]: baseValue = execAmount / execPrice quoteValue = execAmount amountPrecision = exchange.properties.markets[ ticker.symbol]["precision"]["amount"] amountLimits = exchange.properties.markets[ ticker.symbol]["limits"]["cost"] if execAmount < 1: outputTitle = "Insuficient paper order size" outputMessage = "Order size of {:,.0f} contract is less than the minimum required size of 1 contract.".format( execAmount) return outputTitle, outputMessage, paper, None elif execAmount > amountLimits["max"]: outputTitle = "Paper order size exeeds maximum allowed order size" outputMessage = "Order size must not exceed {:,.0f} {}.".format( amountLimits["max"], base) elif not reduceOnly and ( (orderType.endswith("sell") and baseValue > baseOrder["amount"]) or (orderType.endswith("buy") and quoteValue / execPrice > quoteOrder["amount"])): outputTitle = "Insuficient paper wallet balance" outputMessage = "Order size of {} {} exeeds your paper wallet balance of {:,.8f} {}.".format( execAmountText, base, quoteOrder["amount"] if orderType.endswith("buy") else baseOrder["amount"], quote if orderType.endswith("buy") else base) return outputTitle, outputMessage, paper, None elif (orderType.endswith("buy") and quoteOrder["amount"] == 0) or ( orderType.endswith("sell") and baseOrder["amount"] == 0): outputTitle = "Insuficient paper wallet balance" outputMessage = "Your {} balance is empty.".format( quote if orderType.endswith("buy") else base) return outputTitle, outputMessage, paper, None newOrder = { "id": str(uuid.uuid4()), "orderType": orderType, "request": zlib.compress(pickle.dumps(request, -1)), "amount": execAmount, "price": request.get_numerical_parameters()[0] if isPricePercent else execPrice, "highest": execPrice, "timestamp": int(time.time() * 1000), "status": "placed", "parameters": [isPricePercent, isLimitOrder, reduceOnly] } priceText = "{:,.2f} %".format(request.get_numerical_parameters( )[0]) if isPricePercent else "{} {}".format( execPriceText, ticker.quote) conversionText = None if isPricePercent else "{} contracts ≈ {:,.6f} {}".format( execAmountText, baseValue, ticker.base) return None, None, paper, Order(newOrder, request, priceText=priceText, conversionText=conversionText, amountText=execAmountText) else: baseValue = execAmount quoteValue = execAmount * execPrice amountPrecision = exchange.properties.markets[ ticker.symbol]["precision"]["amount"] amountLimits = exchange.properties.markets[ ticker.symbol]["limits"]["amount"] if execAmount < amountLimits["min"]: outputTitle = "Insuficient paper order size" outputMessage = ( "Order size of {:,.%df} {} is less than the minimum required size of {:,.%df} {}." % (amountPrecision, amountPrecision)).format( execAmount, quote, amountLimits["min"], base) return outputTitle, outputMessage, paper, None elif execAmount > amountLimits["max"]: outputTitle = "Paper order size exeeds maximum allowed order size" outputMessage = ("Order size must not exceed {:,.%df} {}." % (amountPrecision)).format( amountLimits["max"], base) return outputTitle, outputMessage, paper, None elif not reduceOnly and ((orderType.endswith("sell") and baseValue > baseOrder["amount"]) or (orderType.endswith("buy") and quoteValue > quoteOrder["amount"])): outputTitle = "Insuficient paper wallet balance" outputMessage = "Order size of {} {} exeeds your paper wallet balance of {:,.8f} {}.".format( execAmountText, base, quoteOrder["amount"] if orderType.endswith("buy") else baseOrder["amount"], quote if orderType.endswith("buy") else base) return outputTitle, outputMessage, paper, None elif (orderType.endswith("buy") and quoteOrder["amount"] == 0) or ( orderType.endswith("sell") and baseOrder["amount"] == 0): outputTitle = "Insuficient paper wallet balance" outputMessage = "Your {} balance is empty.".format( quote if orderType.endswith("buy") else base) return outputTitle, outputMessage, paper, None newOrder = { "id": str(uuid.uuid4()), "orderType": orderType, "request": zlib.compress(pickle.dumps(request, -1)), "amount": execAmount, "price": request.get_numerical_parameters()[0] if isPricePercent else execPrice, "highest": execPrice, "timestamp": int(time.time() * 1000), "status": "placed", "parameters": [isPricePercent, isLimitOrder, reduceOnly] } priceText = "{:,.2f} %".format(request.get_numerical_parameters( )[0]) if isPricePercent else "{} {}".format( execPriceText, ticker.quote) conversionText = None if isPricePercent else "{} {} ≈ {:,.6f} {}".format( execAmountText, ticker.base, quoteValue, ticker.quote) return None, None, paper, Order(newOrder, request, priceText=priceText, conversionText=conversionText, amountText=execAmountText)
def update_paper_limit_orders(self): """Process paper limit orders """ socket = CronJobs.zmqContext.socket(zmq.REQ) socket.connect("tcp://candle-server:6900") socket.setsockopt(zmq.LINGER, 3) poller = zmq.Poller() poller.register(socket, zmq.POLLIN) try: for accountId in self.accountProperties: if "customer" in self.accountProperties[accountId]: for exchange in self.accountProperties[accountId][ "paperTrader"]: if exchange in ["globalLastReset", "globalResetCount"]: continue paper = self.accountProperties[accountId][ "paperTrader"][exchange] for order in list(paper["openOrders"]): paperRequest = pickle.loads( zlib.decompress(order["request"])) ticker = paperRequest.get_ticker() exchange = paperRequest.get_exchange() if paperRequest.currentPlatform == "CCXT": levelText = Utils.format_price( exchange.properties, ticker.symbol, order["price"]) elif paperRequest.currentPlatform == "IEXC" or paperRequest.currentPlatform == "Quandl": levelText = "{:,.5f}".format(order["price"]) else: levelText = "{:,.0f}".format(order["price"]) socket.send_multipart( [b"cronjob", b"candle", order["request"]]) responses = poller.poll(5 * 1000) if len(responses) != 0: response = socket.recv() payload, responseText = pickle.loads( zlib.decompress(response)) if payload is None: if responseText is not None: print("Paper order request error", responseText) if os.environ["PRODUCTION_MODE"]: self.logging.report(responseText) return for candle in reversed(payload["candles"]): if candle[0] < order["timestamp"] / 1000: break if candle[3] < order["price"] < candle[2]: baseOrder = paper["balance"][ ticker.base] quoteOrder = paper["balance"][ ticker.quote] execAmount = order["amount"] isPricePercent, isLimitOrder, reduceOnly = order[ "parameters"] if reduceOnly and ( (order["orderType"] == "buy" and baseOrder["amount"] >= 0) or (order["orderType"] == "sell" and baseOrder["amount"] <= 0)): order["status"] = "canceled" paper["openOrders"].remove(order) if exchange.id == "bitmex": averageEntry = ( baseOrder["entry"] * baseOrder["amount"] + order["price"] * execAmount ) / ( baseOrder["amount"] + execAmount ) if baseOrder[ "amount"] + execAmount != 0 else 0 quoteValue = ( abs(execAmount) * (-1 if reduceOnly else 1) ) / (averageEntry if averageEntry != 0 else baseOrder["entry"]) / leverage roi = ( (order["price"] - baseOrder["entry"]) * 0.000001 if ticker.symbol == "ETH/USD" else (1 / baseOrder["entry"] - 1 / order["price"])) * baseOrder[ "amount"] if baseOrder[ "entry"] != 0 else 0 orderFee = execAmount * exchange.properties.markets[ ticker. symbol]["maker" if isLimitOrder else "taker"] if order[ "orderType"] == "buy" or order[ "orderType"] == "sell": baseOrder[ "entry"] = averageEntry baseOrder[ "amount"] += execAmount elif order[ "orderType"] == "stop-buy" or order[ "orderType"] == "stop-sell": quoteOrder["amount"] += round( roi - (quoteValue + abs(orderFee) / order["price"]), 8) baseOrder[ "entry"] = averageEntry baseOrder[ "amount"] += execAmount else: if order["orderType"] == "buy": if reduceOnly: execAmount = min( abs(quoteOrder[ "amount"]), order["price"] * execAmount ) / order["price"] orderFee = execAmount * exchange.properties.markets[ ticker.symbol]["maker"] baseOrder[ "amount"] += execAmount - orderFee elif order["orderType"] == "sell": if reduceOnly: execAmount = min( abs(baseOrder["amount"] ), execAmount) orderFee = execAmount * exchange.properties.markets[ ticker.symbol]["maker"] quoteOrder["amount"] += ( execAmount - orderFee) * order["price"] elif order[ "orderType"] == "stop-buy": if reduceOnly: execAmount = min( abs(quoteOrder[ "amount"]), order["price"] * execAmount ) / order["price"] orderFee = execAmount * exchange.properties.markets[ ticker.symbol]["taker"] baseOrder[ "amount"] += execAmount - orderFee quoteOrder["amount"] -= order[ "price"] * execAmount elif order[ "orderType"] == "stop-sell": if reduceOnly: execAmount = min( abs(baseOrder["amount"] ), execAmount) orderFee = execAmount * exchange.properties.markets[ ticker.symbol]["taker"] baseOrder[ "amount"] -= execAmount quoteOrder["amount"] += ( execAmount - orderFee) * order["price"] paper["openOrders"].remove(order) order["status"] = "filled" paper["history"].append(order) database.document("accounts/{}".format( accountId)).set( { "paperTrader": { exchange.id: paper } }, merge=True) if self.server.accountProperties[ accountId]["oauth"][ "discord"].get( "userId") is not None: database.document( "discord/properties/messages/{}" .format(str(uuid.uuid4())) ).set({ "title": "Paper {} order of {} {} on {} at {} was successfully executed." .format( order["orderType"].replace( "-", " "), Utils.format_amount( exchange.properties, ticker.symbol, order["amount"]), order["base"], exchange.name, order["price"]), "subtitle": "Alpha Paper Trader", "description": None, "color": 6765239, "user": self.server. accountProperties[accountId] ["oauth"]["discord"]["userId"], "channel": "611107823111372810" }) except (KeyboardInterrupt, SystemExit): pass except Exception: print(traceback.format_exc()) if os.environ["PRODUCTION_MODE"]: self.logging.report_exception()
def check_paper_order(self, authorId, accountId, reference, order): socket = CronJobs.zmqContext.socket(zmq.REQ) socket.connect("tcp://candle-server:6900") socket.setsockopt(zmq.LINGER, 3) poller = zmq.Poller() poller.register(socket, zmq.POLLIN) try: paperRequest = pickle.loads(zlib.decompress(order["request"])) paperRequest.timestamp = time.time() ticker = paperRequest.get_ticker() exchange = paperRequest.get_exchange() if paperRequest.currentPlatform == "CCXT": levelText = Utils.format_price(exchange.properties, ticker.symbol, order["price"]) elif paperRequest.currentPlatform == "IEXC": levelText = "{:,.5f}".format(order["price"]) else: levelText = "{:,.0f}".format(order["price"]) if order["timestamp"] < time.time() - 86400 * 30.5 * 6: if os.environ["PRODUCTION_MODE"]: database.document("discord/properties/messages/{}".format( str(uuid.uuid4()) )).set({ "title": "Paper {} order of {} {} on {} at {} {} expired.". format( order["orderType"].replace("-", " "), Utils.format_amount(exchange.properties, ticker.symbol, order["amount"]), ticker.base, paperRequest.currentPlatform if exchange is None else exchange.name, order["price"], ticker.quote), "subtitle": "Alpha Paper Trader", "description": "Paper orders automatically cancel after 6 months. If you'd like to keep your order, you'll have to set it again.", "color": 6765239, "user": authorId, "channel": order["channel"] }) reference.delete() else: print("{}: paper {} order of {} {} on {} at {} expired". format( order["orderType"].replace("-", " "), Utils.format_amount(exchange.properties, ticker.symbol, order["amount"]), ticker.base, paperRequest.currentPlatform if exchange is None else exchange.name, order["price"], ticker.quote)) else: socket.send_multipart([ b"cronjob", b"candle", zlib.compress(pickle.dumps(paperRequest, -1)) ]) responses = poller.poll(30 * 1000) if len(responses) != 0: response = socket.recv() payload, responseText = pickle.loads( zlib.decompress(response)) if payload is None: if responseText is not None: print("Paper order request error", responseText) if os.environ["PRODUCTION_MODE"]: self.logging.report(responseText) return paperRequest.set_current(platform=payload["platform"]) for candle in reversed(payload["candles"]): if candle[0] < order["timestamp"]: break if (candle[3] <= order["level"] and order["placement"] == "below") or (order["level"] <= candle[2] and order["placement"] == "above"): loop = asyncio.get_event_loop() accountProperties = loop.run_until_complete( self.accountProperties.get(accountId)) if os.environ["PRODUCTION_MODE"]: if "paperTrader" in accountProperties: paper = accountProperties["paperTrader"] baseOrder = paper.get(ticker.base, 0) quoteOrder = paper.get(ticker.quote, 0) execAmount = order["amount"] isPricePercent, isLimitOrder, reduceOnly = order[ "parameters"] if reduceOnly and ( (order["orderType"] == "buy" and baseOrder["amount"] >= 0) or (order["orderType"] == "sell" and baseOrder["amount"] <= 0)): order["status"] = "canceled" database.document( "discord/properties/messages/{}". format(str(uuid.uuid4())) ).set({ "title": "Paper {} order of {} {} on {} at {} {} expired." .format( order["orderType"].replace( "-", " "), Utils.format_amount( exchange.properties, ticker.symbol, execAmount), ticker.base, paperRequest.currentPlatform if exchange is None else exchange.name, order["price"], ticker.quote), "subtitle": "Alpha Paper Trader", "description": "Paper orders automatically cancel after 6 months. If you'd like to keep your order, you'll have to set it again.", "color": 6765239, "user": authorId, "channel": order["channel"] }) reference.delete() else: if order["orderType"] == "buy": if reduceOnly: execAmount = min( abs(quoteOrder["amount"]), order["price"] * execAmount ) / order["price"] orderFee = execAmount * exchange.properties.markets[ ticker.symbol]["maker"] baseOrder[ "amount"] += execAmount - orderFee elif order["orderType"] == "sell": if reduceOnly: execAmount = min( abs(baseOrder["amount"]), execAmount) orderFee = execAmount * exchange.properties.markets[ ticker.symbol]["maker"] quoteOrder["amount"] += ( execAmount - orderFee) * order["price"] elif order["orderType"] == "stop-buy": if reduceOnly: execAmount = min( abs(quoteOrder["amount"]), order["price"] * execAmount ) / order["price"] orderFee = execAmount * exchange.properties.markets[ ticker.symbol]["taker"] baseOrder[ "amount"] += execAmount - orderFee quoteOrder["amount"] -= order[ "price"] * execAmount elif order["orderType"] == "stop-sell": if reduceOnly: execAmount = min( abs(baseOrder["amount"]), execAmount) orderFee = execAmount * exchange.properties.markets[ ticker.symbol]["taker"] baseOrder["amount"] -= execAmount quoteOrder["amount"] += ( execAmount - orderFee) * order["price"] order["status"] = "filled" database.document( "details/paperOrderHistory/{}/{}". format(accountId, str( uuid.uuid4()))).set(order) database.document("accounts/{}".format( accountId)).set( { "paperTrader": { exchange.id: paper } }, merge=True) database.document( "discord/properties/messages/{}". format(str(uuid.uuid4())) ).set({ "title": "Paper {} order of {} {} on {} at {} {} was successfully executed." .format( order["orderType"].replace( "-", " "), Utils.format_amount( exchange.properties, ticker.symbol, execAmount), ticker.base, paperRequest.currentPlatform if exchange is None else exchange.name, order["price"], ticker.quote), "subtitle": "Alpha Paper Trader", "description": None, "color": 6765239, "user": authorId, "channel": order["channel"] }) reference.delete() else: print( "{}: paper {} order of {} {} on {} at {} {} was successfully executed" .format( order["orderType"].replace( "-", " "), Utils.format_amount( exchange.properties, ticker.symbol, order["amount"]), ticker.base, paperRequest.currentPlatform if exchange is None else exchange.name, order["price"], ticker.quote)) break except (KeyboardInterrupt, SystemExit): pass except Exception: print(traceback.format_exc()) if os.environ["PRODUCTION_MODE"]: self.logging.report_exception()