From bb3fb692df57362ff0abb4f2fad20eb444415f20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20S=C3=A1nchez?= Date: Tue, 2 Sep 2025 16:04:52 -0300 Subject: [PATCH] /force_trader_close --- changelog.txt | 2 +- main.py | 52 +++++++++++++++++++++++++++++++++++++++++++++- trader.py | 39 +++++++++++++++++++++++++++++++--- utils/commander.py | 18 ++++++++++++++-- 4 files changed, 104 insertions(+), 7 deletions(-) diff --git a/changelog.txt b/changelog.txt index 35ffa95..5a4a21e 100755 --- a/changelog.txt +++ b/changelog.txt @@ -3,7 +3,7 @@ next: . Now the trader supports multiple safety orders at the same time. . Removed forcing orders when importing a trader. Maybe it will be reinstated at a later date. . Removed endpoint /reload_safety_orders. -. New endpoints: /mod_concurrent_safety orders and /mod_boosted_concurrent_safety_orders. +. New endpoints: /mod_concurrent_safety orders, /mod_boosted_concurrent_safety_orders and /force_trader_close. 2025.08.19: . Improved log trimming. diff --git a/main.py b/main.py index 90491e3..1886caa 100644 --- a/main.py +++ b/main.py @@ -18,7 +18,7 @@ import exchange_wrapper import trader -version = "2025.09.01" +version = "2025.09.02" ''' Color definitions. If you want to change them, check the reference at https://en.wikipedia.org/wiki/ANSI_escape_code#Colors @@ -1135,6 +1135,30 @@ def toggle_autoswitch(): return jsonify({'Error': 'Halp'}) +@base_api.route("/force_trader_close", methods=['POST']) +def force_trader_close(): + ''' + POST request + + Parameters: + base: str + quote: str + ''' + + if not "X-API-KEY" in request.headers or not request.headers.get("X-API-KEY") in valid_keys: + return jsonify({'Error': 'API key invalid'}), 401 + try: + if request.json is None: + return jsonify({'Error': 'request.json is None'}) + data = request.json + base = data["base"] + quote = data["quote"] + return unwrapped_force_trader_close(base,quote) + except Exception as e: + print(e) + return jsonify({'Error': 'Halp'}) + + @base_api.route("/toggle_liquidate_after_switch", methods=['POST']) def toggle_liquidate_after_switch(): ''' @@ -2168,6 +2192,32 @@ def unwrapped_toggle_cleanup(base,quote): return jsonify({"Error": "Task failed successfully"}) +def unwrapped_force_trader_close(base,quote): + ''' + Forces a trader to close the position. + + Parameters: + base (str): The base currency of the pair to close + quote (str): The quote currency of the pair to close + + Returns: + jsonify: A jsonified dictionary detailing the outcome of the operation. + ''' + + try: + symbol = f"{base}/{quote}" + for instance in running_traders: + if symbol==instance.status.get_pair(): + outcome = instance.force_close() + if outcome==0: + return jsonify({"Success": "Trader closed position successfully"}) + return jsonify({"Error": "Error while forcing trader to close position"}) + return jsonify({"Error": "Trader not found"}) + except Exception as e: + broker.logger.log_this(f"Exception while forcing trader to close position: {e}",1,symbol) + return jsonify({"Error": "Halp"}) + + def unwrapped_toggle_autoswitch(base,quote): ''' Signals a trader to enable or disable autoswitch. diff --git a/trader.py b/trader.py index af1bdf5..89dffea 100755 --- a/trader.py +++ b/trader.py @@ -702,6 +702,39 @@ class trader: return 0 + def force_close(self): + ''' + This method forces the closing of a deal. It replaces the take profit order with a market order. + A simpler way of doing it would be to edit the price of the take profit order, + but KuCoin only supports order editing on high frequency orders. + ''' + + self.pause = True + self.status.set_pause_reason("force_close - order handling") + + #Close the take profit order + self.broker.cancel_order(self.status.get_take_profit_order()["id"],self.status.get_pair()) + + #Send the market order + amount = self.status.get_take_profit_order()["amount"] + market_order = self.broker.new_market_order(self.status.get_pair(),amount,"sell",amount_in_base=True) + + #Wait for it to be filled + tries = self.broker.get_retries() + while True: + order = self.broker.get_order(market_order["id"],self.status.get_pair()) + if order["status"]=="closed": + break + tries-=1 + if tries==0: + self.broker.logger.log_this("Forced market order not filling.",1,self.status.get_pair()) + self.quit = True + return 1 + + #Call take profit routine + return self.take_profit_routine(order) + + def take_profit_routine(self, filled_order: dict) -> int: ''' When profit is reached, this method is called to handle the profit calculations, the closing of orders, @@ -715,7 +748,6 @@ class trader: self.deals_timestamps.append(time.time()) self.deals_timestamps = self.deals_timestamps[-self.config.get_boosted_deals_range():] - #Let's do some type checking first if self.status.get_take_profit_order() is None: self.status.set_pause_reason(time.strftime(f"[%Y/%m/%d %H:%M:%S] | {self.status.get_pair()} | TP order is None")) @@ -731,6 +763,7 @@ class trader: #Cancel all the safety orders ASAP for order in self.status.get_safety_orders(): self.broker.cancel_order(order["id"],self.status.get_pair()) + #Check if some safety orders were filled for order in self.status.get_safety_orders(): closed_order = self.broker.get_order(order["id"],self.status.get_pair()) @@ -738,6 +771,7 @@ class trader: #If this order wasn't filled, it is safe to assume that no order coming after this one was. break #Sum the filled amounts + #Better than this, the total filled and total cost can be used to send a sell market order of the partial filled amount, and add that to the profit. self.broker.logger.log_this(f"Old safety order is partially filled, ID: {closed_order['id']}, {closed_order['filled']}/{closed_order['amount']} {self.base} filled",1,self.status.get_pair()) self.status.set_base_bought(self.status.get_base_bought() + closed_order["filled"] - self.parse_fees(closed_order)[0]) self.status.set_quote_spent(self.status.get_quote_spent() + closed_order["cost"]) @@ -749,8 +783,7 @@ class trader: break #Now we can clear the safety order list self.status.set_safety_orders([]) - - + if not self.broker.check_for_duplicate_profit_in_db(filled_order): self.status.set_pause_reason("calculating profit") # Calculate the profit diff --git a/utils/commander.py b/utils/commander.py index 30c7c89..c086531 100644 --- a/utils/commander.py +++ b/utils/commander.py @@ -64,7 +64,7 @@ TRADERS 68) toggle_check_old_long_price 69) switch_quote_currency 70) view_old_long 71) switch_price 72) reload_trader_config 73) toggle_liquidate_after_switch 74) base_add_calculation -75) mod_concurrent_safety_orders +75) mod_concurrent_safety_orders 76) force_trader_close 98) Change broker 99) Exit ''' @@ -880,4 +880,18 @@ if __name__=="__main__": "quote": quote, "amount": new_amount} print(json.loads(requests.post(url, headers=headers, json=parameters).content)) - input("Press ENTER to continue ") \ No newline at end of file + input("Press ENTER to continue ") + + elif command==76: + print("force_trader_close forces a trader to close the current position") + trading_pair = input("Input trader in the format BASE/QUOTE: ").upper() + if not validate_pair(trading_pair): + print("The input is invalid") + break + if input("Proceed? (Y/n) ") in ["Y","y",""]: + url = f"{base_url}{port}/force_trader_close" + base,quote = trading_pair.split("/") + parameters = {"base": base, + "quote": quote} + print(json.loads(requests.post(url, headers=headers, json=parameters).content)) + input("Press ENTER to continue ") \ No newline at end of file