diff --git a/scripts/createoffers.py b/scripts/createoffers.py index 07bc2ab..1062a7e 100755 --- a/scripts/createoffers.py +++ b/scripts/createoffers.py @@ -15,6 +15,9 @@ Create offers "min_seconds_between_bids": Add a random delay between creating bids between min and max, default 60. "max_seconds_between_bids": ^, default "min_seconds_between_bids" * 4 "wallet_port_override": Used for testing. + "prune_state_delay": Seconds between pruning old state data, set to 0 to disable pruning. + "main_loop_delay": Seconds between main loop iterations. + "prune_state_after_seconds": Seconds to keep old state data for. "offers": [ { "name": Offer template name, eg "Offer 0", will be automatically renamed if not unique. @@ -58,21 +61,25 @@ Create offers """ -__version__ = "0.2" +__version__ = "0.3" -import os +import argparse import json -import time +import os import random import shutil import signal -import urllib -import logging -import argparse +import sys import threading +import time +import traceback +import urllib from urllib.request import urlopen delay_event = threading.Event() +coins_map = {} +read_json_api = None +read_json_api_wallet = None DEFAULT_CONFIG_FILE: str = "createoffers.json" DEFAULT_STATE_FILE: str = "createoffers_state.json" @@ -107,7 +114,7 @@ def make_json_api_func(host: str, port: int): def signal_handler(sig, frame) -> None: - logging.info("Signal {} detected.".format(sig)) + os.write(sys.stdout.fileno(), f"Signal {sig} detected.\n".encode("utf-8")) delay_event.set() @@ -235,6 +242,26 @@ def readConfig(args, known_coins): bid_template["coin_to"] = findCoin(bid_template["coin_to"], known_coins) config["num_enabled_bids"] = num_enabled + config["main_loop_delay"] = config.get("main_loop_delay", 60) + if config["main_loop_delay"] < 10: + print("Setting main_loop_delay to 10") + config["main_loop_delay"] = 10 + num_changes += 1 + if config["main_loop_delay"] > 1000: + print("Setting main_loop_delay to 1000") + config["main_loop_delay"] = 1000 + num_changes += 1 + config["prune_state_delay"] = config.get("prune_state_delay", 120) + + seconds_in_day: int = 86400 + config["prune_state_after_seconds"] = config.get( + "prune_state_after_seconds", seconds_in_day * 7 + ) + if config["prune_state_after_seconds"] < seconds_in_day: + print(f"Setting prune_state_after_seconds to {seconds_in_day}") + config["prune_state_after_seconds"] = seconds_in_day + num_changes += 1 + if num_changes > 0: shutil.copyfile(config_path, config_path + ".last") with open(config_path, "w") as fp: @@ -250,7 +277,495 @@ def write_state(statefile, script_state): json.dump(script_state, fp, indent=4) +def process_offers(args, config, script_state) -> None: + offer_templates = config["offers"] + if len(offer_templates) < 1: + return + if args.debug: + print( + "Processing {} offer template{}".format( + config["num_enabled_offers"], + "s" if config["num_enabled_offers"] != 1 else "", + ) + ) + + random.shuffle(offer_templates) + sent_offers = read_json_api("sentoffers", {"active": "active"}) + for offer_template in offer_templates: + if offer_template.get("enabled", True) is False: + continue + offers_found = 0 + + coin_from_data = coins_map[offer_template["coin_from"]] + coin_to_data = coins_map[offer_template["coin_to"]] + + wallet_from = read_json_api_wallet( + "wallets/{}".format(coin_from_data["ticker"]) + ) + coin_ticker = coin_from_data["ticker"] + if coin_ticker == "PART" and "variant" in coin_from_data: + coin_variant = coin_from_data["variant"] + if coin_variant == "Anon": + coin_from_data_name = "PART_ANON" + wallet_balance: float = float(wallet_from["anon_balance"]) + elif coin_variant == "Blind": + coin_from_data_name = "PART_BLIND" + wallet_balance: float = float(wallet_from["blind_balance"]) + else: + raise ValueError(f"{coin_ticker} variant {coin_variant} not handled") + else: + coin_from_data_name = coin_ticker + wallet_balance: float = float(wallet_from["balance"]) + + for offer in sent_offers: + created_offers = script_state.get("offers", {}) + prev_template_offers = created_offers.get(offer_template["name"], {}) + + if next( + (x for x in prev_template_offers if x["offer_id"] == offer["offer_id"]), + None, + ): + offers_found += 1 + if wallet_balance <= float(offer_template["min_coin_from_amt"]): + offer_id = offer["offer_id"] + print( + "Revoking offer {}, wallet from balance below minimum".format( + offer_id + ) + ) + result = read_json_api(f"revokeoffer/{offer_id}") + print("revokeoffer", result) + + if offers_found > 0: + continue + + max_offer_amount: float = offer_template["amount"] + min_offer_amount: float = offer_template.get("amount_step", max_offer_amount) + + min_wallet_from_amount: float = float(offer_template["min_coin_from_amt"]) + if wallet_balance - min_offer_amount <= min_wallet_from_amount: + print( + "Skipping template {}, wallet from balance below minimum".format( + offer_template["name"] + ) + ) + continue + + offer_amount: float = max_offer_amount + if wallet_balance - max_offer_amount <= min_wallet_from_amount: + available_balance: float = wallet_balance - min_wallet_from_amount + min_steps: int = available_balance // min_offer_amount + assert min_steps > 0 # Should not be possible, checked above + offer_amount = min_offer_amount * min_steps + + delay_next_offer_before = script_state.get("delay_next_offer_before", 0) + if delay_next_offer_before > int(time.time()): + print("Delaying offers until {}".format(delay_next_offer_before)) + break + + """ + received_offers = read_json_api(args.port, 'offers', {'active': 'active', 'include_sent': False, 'coin_from': coin_from_data['id'], 'coin_to': coin_to_data['id']}) + print('received_offers', received_offers) + + TODO - adjust rates based on existing offers + """ + + rates = read_json_api( + "rates", + {"coin_from": coin_from_data["id"], "coin_to": coin_to_data["id"]}, + ) + print("Rates", rates) + coingecko_rate = float(rates["coingecko"]["rate_inferred"]) + use_rate = coingecko_rate + + if offer_template["ratetweakpercent"] != 0: + print( + "Adjusting rate {} by {}%.".format( + use_rate, offer_template["ratetweakpercent"] + ) + ) + tweak = offer_template["ratetweakpercent"] / 100.0 + use_rate += use_rate * tweak + + if use_rate < offer_template["minrate"]: + print("Warning: Clamping rate to minimum.") + use_rate = offer_template["minrate"] + + print("Creating offer for: {} at rate: {}".format(offer_template, use_rate)) + template_from_addr = offer_template["address"] + offer_data = { + "addr_from": (-1 if template_from_addr == "auto" else template_from_addr), + "coin_from": coin_from_data_name, + "coin_to": coin_to_data["ticker"], + "amt_from": offer_amount, + "amt_var": offer_template["amount_variable"], + "valid_for_seconds": offer_template.get( + "offer_valid_seconds", config.get("offer_valid_seconds", 3600) + ), + "rate": use_rate, + "swap_type": offer_template.get("swap_type", "adaptor_sig"), + "lockhrs": "24", + "automation_strat_id": 1, + } + if "min_swap_amount" in offer_template: + offer_data["amt_bid_min"] = offer_template["min_swap_amount"] + if args.debug: + print("offer data {}".format(offer_data)) + new_offer = read_json_api("offers/new", offer_data) + if "error" in new_offer: + raise ValueError( + "Server failed to create offer: {}".format(new_offer["error"]) + ) + print("New offer: {}".format(new_offer["offer_id"])) + if "offers" not in script_state: + script_state["offers"] = {} + template_name = offer_template["name"] + if template_name not in script_state["offers"]: + script_state["offers"][template_name] = [] + script_state["offers"][template_name].append( + {"offer_id": new_offer["offer_id"], "time": int(time.time())} + ) + max_seconds_between_offers = config["max_seconds_between_offers"] + min_seconds_between_offers = config["min_seconds_between_offers"] + time_between_offers = min_seconds_between_offers + if max_seconds_between_offers > min_seconds_between_offers: + time_between_offers = random.randint( + min_seconds_between_offers, max_seconds_between_offers + ) + + script_state["delay_next_offer_before"] = int(time.time()) + time_between_offers + write_state(args.statefile, script_state) + + +def process_bids(args, config, script_state) -> None: + bid_templates = config["bids"] + if len(bid_templates) < 1: + return + random.shuffle(bid_templates) + + if args.debug: + print( + "Processing {} bid template{}".format( + config["num_enabled_bids"], + "s" if config["num_enabled_bids"] != 1 else "", + ) + ) + for bid_template in bid_templates: + if bid_template.get("enabled", True) is False: + continue + delay_next_bid_before = script_state.get("delay_next_bid_before", 0) + if delay_next_bid_before > int(time.time()): + print("Delaying bids until {}".format(delay_next_bid_before)) + break + + # Check bids in progress + max_concurrent = bid_template.get("max_concurrent", 1) + if "bids" not in script_state: + script_state["bids"] = {} + template_name = bid_template["name"] + if template_name not in script_state["bids"]: + script_state["bids"][template_name] = [] + previous_bids = script_state["bids"][template_name] + + bids_in_progress: int = 0 + for previous_bid in previous_bids: + if not previous_bid["active"]: + continue + previous_bid_id = previous_bid["bid_id"] + previous_bid_info = read_json_api(f"bids/{previous_bid_id}") + bid_state = previous_bid_info["bid_state"] + if bid_state in ( + "Completed", + "Timed-out", + "Abandoned", + "Error", + "Rejected", + ): + print(f"Marking bid inactive {previous_bid_id}, state {bid_state}") + previous_bid["active"] = False + write_state(args.statefile, script_state) + continue + if bid_state in ("Sent", "Received") and previous_bid_info[ + "expired_at" + ] < int(time.time()): + print(f"Marking bid inactive {previous_bid_id}, expired") + previous_bid["active"] = False + write_state(args.statefile, script_state) + continue + bids_in_progress += 1 + + if bids_in_progress >= max_concurrent: + print("Max concurrent bids reached for template") + continue + + # Bidder sends coin_to and receives coin_from + coin_from_data = coins_map[bid_template["coin_from"]] + coin_to_data = coins_map[bid_template["coin_to"]] + + page_limit: int = 25 + offers_options = { + "active": "active", + "include_sent": False, + "coin_from": coin_from_data["id"], + "coin_to": coin_to_data["id"], + "with_extra_info": True, + "sort_by": "rate", + "sort_dir": "asc", + "offset": 0, + "limit": page_limit, + } + + received_offers = [] + for i in range(1000000): # for i in itertools.count() + page_offers = read_json_api("offers", offers_options) + if len(page_offers) < 1: + break + received_offers += page_offers + offers_options["offset"] = offers_options["offset"] + page_limit + if i > 100: + print(f"Warning: Broke offers loop at: {i}") + break + + if args.debug: + print("Received Offers", received_offers) + + for offer in received_offers: + offer_id = offer["offer_id"] + offer_amount = float(offer["amount_from"]) + offer_rate = float(offer["rate"]) + bid_amount = bid_template["amount"] + + min_swap_amount = bid_template.get( + "min_swap_amount", 0.01 + ) # TODO: Make default vary per coin + can_adjust_offer_amount: bool = offer["amount_negotiable"] + can_adjust_bid_amount: bool = bid_template.get("amount_variable", True) + can_adjust_amount: bool = can_adjust_offer_amount and can_adjust_bid_amount + + if offer_amount < min_swap_amount: + if args.debug: + print(f"Offer amount below min swap amount bid {offer_id}") + continue + + if can_adjust_offer_amount is False and offer_amount > bid_amount: + if args.debug: + print(f"Bid amount too low for offer {offer_id}") + continue + + if bid_amount > offer_amount: + if can_adjust_bid_amount: + bid_amount = offer_amount + else: + if args.debug: + print(f"Bid amount too high for offer {offer_id}") + continue + + if offer_rate > bid_template["maxrate"]: + if args.debug: + print(f"Bid rate too low for offer {offer_id}") + continue + + sent_bids = read_json_api( + "sentbids", + { + "offer_id": offer["offer_id"], + "with_available_or_active": True, + }, + ) + if len(sent_bids) > 0: + if args.debug: + print(f"Already bidding on offer {offer_id}") + continue + + offer_identity = read_json_api("identities/{}".format(offer["addr_from"])) + if "address" in offer_identity: + id_offer_from = offer_identity + automation_override = id_offer_from["automation_override"] + if automation_override == 2: + if args.debug: + print( + f"Not bidding on offer {offer_id}, automation_override ({automation_override})." + ) + continue + if automation_override == 1: + if args.debug: + print( + "Offer address from {}, set to always accept.".format( + offer["addr_from"] + ) + ) + else: + successful_sent_bids = id_offer_from["num_sent_bids_successful"] + failed_sent_bids = id_offer_from["num_sent_bids_failed"] + if failed_sent_bids > 3 and failed_sent_bids > successful_sent_bids: + if args.debug: + print( + f"Not bidding on offer {offer_id}, too many failed bids ({failed_sent_bids})." + ) + continue + + validateamount: bool = False + max_coin_from_balance = bid_template.get("max_coin_from_balance", -1) + if max_coin_from_balance > 0: + wallet_from = read_json_api_wallet( + "wallets/{}".format(coin_from_data["ticker"]) + ) + total_balance_from = float(wallet_from["balance"]) + float( + wallet_from["unconfirmed"] + ) + if args.debug: + print(f"Total coin from balance {total_balance_from}") + if total_balance_from + bid_amount > max_coin_from_balance: + if ( + can_adjust_amount + and max_coin_from_balance - total_balance_from > min_swap_amount + ): + bid_amount = max_coin_from_balance - total_balance_from + validateamount = True + print(f"Reduced bid amount to {bid_amount}") + else: + if args.debug: + print( + f"Bid amount would exceed maximum wallet total for offer {offer_id}" + ) + continue + + min_coin_to_balance = bid_template["min_coin_to_balance"] + if min_coin_to_balance > 0: + wallet_to = read_json_api_wallet( + "wallets/{}".format(coin_to_data["ticker"]) + ) + + total_balance_to = float(wallet_to["balance"]) + float( + wallet_to["unconfirmed"] + ) + if args.debug: + print(f"Total coin to balance {total_balance_to}") + + swap_amount_to = bid_amount * offer_rate + if total_balance_to - swap_amount_to < min_coin_to_balance: + if can_adjust_amount: + adjusted_swap_amount_to = total_balance_to - min_coin_to_balance + adjusted_bid_amount = adjusted_swap_amount_to / offer_rate + + if adjusted_bid_amount > min_swap_amount: + bid_amount = adjusted_bid_amount + validateamount = True + print(f"Reduced bid amount to {bid_amount}") + swap_amount_to = adjusted_bid_amount * offer_rate + + if total_balance_to - swap_amount_to < min_coin_to_balance: + if args.debug: + print( + f"Bid amount would exceed minimum coin to wallet total for offer {offer_id}" + ) + continue + + if validateamount: + bid_amount = read_json_api( + "validateamount", + { + "coin": coin_from_data["ticker"], + "amount": bid_amount, + "method": "rounddown", + }, + ) + bid_data = { + "offer_id": offer["offer_id"], + "amount_from": bid_amount, + } + + if "address" in bid_template: + addr_from = bid_template["address"] + if addr_from != -1 and addr_from != "auto": + bid_data["addr_from"] = addr_from + + if config.get("test_mode", False): + print("Would create bid: {}".format(bid_data)) + bid_id = "simulated" + else: + if args.debug: + print("Creating bid: {}".format(bid_data)) + new_bid = read_json_api("bids/new", bid_data) + if "error" in new_bid: + raise ValueError( + "Server failed to create bid: {}".format(new_bid["error"]) + ) + print( + "New bid: {} on offer {}".format( + new_bid["bid_id"], offer["offer_id"] + ) + ) + bid_id = new_bid["bid_id"] + + script_state["bids"][template_name].append( + {"bid_id": bid_id, "time": int(time.time()), "active": True} + ) + + max_seconds_between_bids = config["max_seconds_between_bids"] + min_seconds_between_bids = config["min_seconds_between_bids"] + if max_seconds_between_bids > min_seconds_between_bids: + time_between_bids = random.randint( + min_seconds_between_bids, max_seconds_between_bids + ) + else: + time_between_bids = min_seconds_between_bids + script_state["delay_next_bid_before"] = int(time.time()) + time_between_bids + write_state(args.statefile, script_state) + break # Create max one bid per iteration + + +def prune_script_state(now, args, config, script_state): + if args.debug: + print("Pruning script state.") + + removed_offers: int = 0 + removed_bids: int = 0 + + max_ttl: int = config["prune_state_after_seconds"] + if "offers" in script_state: + for template_name, template_group in script_state["offers"].items(): + offers_to_remove = [] + for offer in template_group: + if now - offer["time"] > max_ttl: + offers_to_remove.append(offer["offer_id"]) + + for offer_id in offers_to_remove: + for i, offer in enumerate(template_group): + if offer_id == offer["offer_id"]: + del template_group[i] + removed_offers += 1 + break + + if "bids" in script_state: + for template_name, template_group in script_state["bids"].items(): + bids_to_remove = [] + for bid in template_group: + if now - bid["time"] > max_ttl: + bids_to_remove.append(bid["bid_id"]) + + for bid_id in bids_to_remove: + for i, bid in enumerate(template_group): + if bid_id == bid["bid_id"]: + del template_group[i] + removed_bids += 1 + break + + if removed_offers > 0 or removed_bids > 0: + print( + "Pruned {} offer{} and {} bid{} from script state.".format( + removed_offers, + "s" if removed_offers != 1 else "", + removed_bids, + "s" if removed_bids != 1 else "", + ) + ) + script_state["time_last_pruned_state"] = now + write_state(args.statefile, script_state) + + def main(): + global read_json_api, read_json_api_wallet, coins_map parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "-v", @@ -312,7 +827,6 @@ def main(): raise ValueError(f'Config file "{args.configfile}" not found.') known_coins = read_json_api("coins") - coins_map = {} for known_coin in known_coins: coins_map[known_coin["name"]] = known_coin @@ -323,13 +837,8 @@ def main(): signal.signal(signal.SIGINT, signal_handler) while not delay_event.is_set(): - # Read config each iteration so they can be modified without restarting + # Read config each iteration so it can be modified without restarting config = readConfig(args, known_coins) - offer_templates = config["offers"] - random.shuffle(offer_templates) - - bid_templates = config["bids"] - random.shuffle(bid_templates) # override wallet api calls for testing if "wallet_port_override" in config: @@ -340,482 +849,28 @@ def main(): read_json_api_wallet = read_json_api try: - sent_offers = read_json_api("sentoffers", {"active": "active"}) + process_offers(args, config, script_state) - if args.debug and len(offer_templates) > 0: - print( - "Processing {} offer template{}".format( - config["num_enabled_offers"], - "s" if config["num_enabled_offers"] != 1 else "", - ) - ) - for offer_template in offer_templates: - if offer_template.get("enabled", True) is False: - continue - offers_found = 0 + process_bids(args, config, script_state) - coin_from_data = coins_map[offer_template["coin_from"]] - coin_to_data = coins_map[offer_template["coin_to"]] - - wallet_from = read_json_api_wallet( - "wallets/{}".format(coin_from_data["ticker"]) - ) - coin_ticker = coin_from_data["ticker"] - if coin_ticker == "PART" and "variant" in coin_from_data: - coin_variant = coin_from_data["variant"] - if coin_variant == "Anon": - coin_from_data_name = "PART_ANON" - wallet_balance: float = float(wallet_from["anon_balance"]) - elif coin_variant == "Blind": - coin_from_data_name = "PART_BLIND" - wallet_balance: float = float(wallet_from["blind_balance"]) - else: - raise ValueError( - f"{coin_ticker} variant {coin_variant} not handled" - ) - else: - coin_from_data_name = coin_ticker - wallet_balance: float = float(wallet_from["balance"]) - - for offer in sent_offers: - created_offers = script_state.get("offers", {}) - prev_template_offers = created_offers.get( - offer_template["name"], {} - ) - - if next( - ( - x - for x in prev_template_offers - if x["offer_id"] == offer["offer_id"] - ), - None, - ): - offers_found += 1 - if wallet_balance <= float(offer_template["min_coin_from_amt"]): - offer_id = offer["offer_id"] - print( - "Revoking offer {}, wallet from balance below minimum".format( - offer_id - ) - ) - result = read_json_api(f"revokeoffer/{offer_id}") - print("revokeoffer", result) - - if offers_found > 0: - continue - - max_offer_amount: float = offer_template["amount"] - min_offer_amount: float = offer_template.get( - "amount_step", max_offer_amount - ) - - min_wallet_from_amount: float = float( - offer_template["min_coin_from_amt"] - ) - if wallet_balance - min_offer_amount <= min_wallet_from_amount: - print( - "Skipping template {}, wallet from balance below minimum".format( - offer_template["name"] - ) - ) - continue - - offer_amount: float = max_offer_amount - if wallet_balance - max_offer_amount <= min_wallet_from_amount: - available_balance: float = wallet_balance - min_wallet_from_amount - min_steps: int = available_balance // min_offer_amount - assert min_steps > 0 # Should not be possible, checked above - offer_amount = min_offer_amount * min_steps - - delay_next_offer_before = script_state.get("delay_next_offer_before", 0) - if delay_next_offer_before > int(time.time()): - print("Delaying offers until {}".format(delay_next_offer_before)) - break - - """ - received_offers = read_json_api(args.port, 'offers', {'active': 'active', 'include_sent': False, 'coin_from': coin_from_data['id'], 'coin_to': coin_to_data['id']}) - print('received_offers', received_offers) - - TODO - adjust rates based on existing offers - """ - - rates = read_json_api( - "rates", - {"coin_from": coin_from_data["id"], "coin_to": coin_to_data["id"]}, - ) - print("Rates", rates) - coingecko_rate = float(rates["coingecko"]["rate_inferred"]) - use_rate = coingecko_rate - - if offer_template["ratetweakpercent"] != 0: - print( - "Adjusting rate {} by {}%.".format( - use_rate, offer_template["ratetweakpercent"] - ) - ) - tweak = offer_template["ratetweakpercent"] / 100.0 - use_rate += use_rate * tweak - - if use_rate < offer_template["minrate"]: - print("Warning: Clamping rate to minimum.") - use_rate = offer_template["minrate"] - - print( - "Creating offer for: {} at rate: {}".format( - offer_template, use_rate - ) - ) - template_from_addr = offer_template["address"] - offer_data = { - "addr_from": ( - -1 if template_from_addr == "auto" else template_from_addr - ), - "coin_from": coin_from_data_name, - "coin_to": coin_to_data["ticker"], - "amt_from": offer_amount, - "amt_var": offer_template["amount_variable"], - "valid_for_seconds": offer_template.get( - "offer_valid_seconds", config.get("offer_valid_seconds", 3600) - ), - "rate": use_rate, - "swap_type": offer_template.get("swap_type", "adaptor_sig"), - "lockhrs": "24", - "automation_strat_id": 1, - } - if "min_swap_amount" in offer_template: - offer_data["amt_bid_min"] = offer_template["min_swap_amount"] - if args.debug: - print("offer data {}".format(offer_data)) - new_offer = read_json_api("offers/new", offer_data) - if "error" in new_offer: - raise ValueError( - "Server failed to create offer: {}".format(new_offer["error"]) - ) - print("New offer: {}".format(new_offer["offer_id"])) - if "offers" not in script_state: - script_state["offers"] = {} - template_name = offer_template["name"] - if template_name not in script_state["offers"]: - script_state["offers"][template_name] = [] - script_state["offers"][template_name].append( - {"offer_id": new_offer["offer_id"], "time": int(time.time())} - ) - max_seconds_between_offers = config["max_seconds_between_offers"] - min_seconds_between_offers = config["min_seconds_between_offers"] - time_between_offers = min_seconds_between_offers - if max_seconds_between_offers > min_seconds_between_offers: - time_between_offers = random.randint( - min_seconds_between_offers, max_seconds_between_offers - ) - - script_state["delay_next_offer_before"] = ( - int(time.time()) + time_between_offers - ) - write_state(args.statefile, script_state) - - if args.debug and len(bid_templates) > 0: - print( - "Processing {} bid template{}".format( - config["num_enabled_bids"], - "s" if config["num_enabled_bids"] != 1 else "", - ) - ) - for bid_template in bid_templates: - if bid_template.get("enabled", True) is False: - continue - delay_next_bid_before = script_state.get("delay_next_bid_before", 0) - if delay_next_bid_before > int(time.time()): - print("Delaying bids until {}".format(delay_next_bid_before)) - break - - # Check bids in progress - max_concurrent = bid_template.get("max_concurrent", 1) - if "bids" not in script_state: - script_state["bids"] = {} - template_name = bid_template["name"] - if template_name not in script_state["bids"]: - script_state["bids"][template_name] = [] - previous_bids = script_state["bids"][template_name] - - bids_in_progress: int = 0 - for previous_bid in previous_bids: - if not previous_bid["active"]: - continue - previous_bid_id = previous_bid["bid_id"] - previous_bid_info = read_json_api(f"bids/{previous_bid_id}") - bid_state = previous_bid_info["bid_state"] - if bid_state in ( - "Completed", - "Timed-out", - "Abandoned", - "Error", - "Rejected", - ): - print( - f"Marking bid inactive {previous_bid_id}, state {bid_state}" - ) - previous_bid["active"] = False - write_state(args.statefile, script_state) - continue - if bid_state in ("Sent", "Received") and previous_bid_info[ - "expired_at" - ] < int(time.time()): - print(f"Marking bid inactive {previous_bid_id}, expired") - previous_bid["active"] = False - write_state(args.statefile, script_state) - continue - bids_in_progress += 1 - - if bids_in_progress >= max_concurrent: - print("Max concurrent bids reached for template") - continue - - # Bidder sends coin_to and receives coin_from - coin_from_data = coins_map[bid_template["coin_from"]] - coin_to_data = coins_map[bid_template["coin_to"]] - - page_limit: int = 25 - offers_options = { - "active": "active", - "include_sent": False, - "coin_from": coin_from_data["id"], - "coin_to": coin_to_data["id"], - "with_extra_info": True, - "sort_by": "rate", - "sort_dir": "asc", - "offset": 0, - "limit": page_limit, - } - - received_offers = [] - for i in range(1000000): # for i in itertools.count() - page_offers = read_json_api("offers", offers_options) - if len(page_offers) < 1: - break - received_offers += page_offers - offers_options["offset"] = offers_options["offset"] + page_limit - if i > 100: - print(f"Warning: Broke offers loop at: {i}") - break - - if args.debug: - print("Received Offers", received_offers) - - for offer in received_offers: - offer_id = offer["offer_id"] - offer_amount = float(offer["amount_from"]) - offer_rate = float(offer["rate"]) - bid_amount = bid_template["amount"] - - min_swap_amount = bid_template.get( - "min_swap_amount", 0.01 - ) # TODO: Make default vary per coin - can_adjust_offer_amount: bool = offer["amount_negotiable"] - can_adjust_bid_amount: bool = bid_template.get( - "amount_variable", True - ) - can_adjust_amount: bool = ( - can_adjust_offer_amount and can_adjust_bid_amount - ) - - if offer_amount < min_swap_amount: - if args.debug: - print(f"Offer amount below min swap amount bid {offer_id}") - continue - - if can_adjust_offer_amount is False and offer_amount > bid_amount: - if args.debug: - print(f"Bid amount too low for offer {offer_id}") - continue - - if bid_amount > offer_amount: - if can_adjust_bid_amount: - bid_amount = offer_amount - else: - if args.debug: - print(f"Bid amount too high for offer {offer_id}") - continue - - if offer_rate > bid_template["maxrate"]: - if args.debug: - print(f"Bid rate too low for offer {offer_id}") - continue - - sent_bids = read_json_api( - "sentbids", - { - "offer_id": offer["offer_id"], - "with_available_or_active": True, - }, - ) - if len(sent_bids) > 0: - if args.debug: - print(f"Already bidding on offer {offer_id}") - continue - - offer_identity = read_json_api( - "identities/{}".format(offer["addr_from"]) - ) - if "address" in offer_identity: - id_offer_from = offer_identity - automation_override = id_offer_from["automation_override"] - if automation_override == 2: - if args.debug: - print( - f"Not bidding on offer {offer_id}, automation_override ({automation_override})." - ) - continue - if automation_override == 1: - if args.debug: - print( - "Offer address from {}, set to always accept.".format( - offer["addr_from"] - ) - ) - else: - successful_sent_bids = id_offer_from[ - "num_sent_bids_successful" - ] - failed_sent_bids = id_offer_from["num_sent_bids_failed"] - if ( - failed_sent_bids > 3 - and failed_sent_bids > successful_sent_bids - ): - if args.debug: - print( - f"Not bidding on offer {offer_id}, too many failed bids ({failed_sent_bids})." - ) - continue - - validateamount: bool = False - max_coin_from_balance = bid_template.get( - "max_coin_from_balance", -1 - ) - if max_coin_from_balance > 0: - wallet_from = read_json_api_wallet( - "wallets/{}".format(coin_from_data["ticker"]) - ) - total_balance_from = float(wallet_from["balance"]) + float( - wallet_from["unconfirmed"] - ) - if args.debug: - print(f"Total coin from balance {total_balance_from}") - if total_balance_from + bid_amount > max_coin_from_balance: - if ( - can_adjust_amount - and max_coin_from_balance - total_balance_from - > min_swap_amount - ): - bid_amount = max_coin_from_balance - total_balance_from - validateamount = True - print(f"Reduced bid amount to {bid_amount}") - else: - if args.debug: - print( - f"Bid amount would exceed maximum wallet total for offer {offer_id}" - ) - continue - - min_coin_to_balance = bid_template["min_coin_to_balance"] - if min_coin_to_balance > 0: - wallet_to = read_json_api_wallet( - "wallets/{}".format(coin_to_data["ticker"]) - ) - - total_balance_to = float(wallet_to["balance"]) + float( - wallet_to["unconfirmed"] - ) - if args.debug: - print(f"Total coin to balance {total_balance_to}") - - swap_amount_to = bid_amount * offer_rate - if total_balance_to - swap_amount_to < min_coin_to_balance: - if can_adjust_amount: - adjusted_swap_amount_to = ( - total_balance_to - min_coin_to_balance - ) - adjusted_bid_amount = ( - adjusted_swap_amount_to / offer_rate - ) - - if adjusted_bid_amount > min_swap_amount: - bid_amount = adjusted_bid_amount - validateamount = True - print(f"Reduced bid amount to {bid_amount}") - swap_amount_to = adjusted_bid_amount * offer_rate - - if total_balance_to - swap_amount_to < min_coin_to_balance: - if args.debug: - print( - f"Bid amount would exceed minimum coin to wallet total for offer {offer_id}" - ) - continue - - if validateamount: - bid_amount = read_json_api( - "validateamount", - { - "coin": coin_from_data["ticker"], - "amount": bid_amount, - "method": "rounddown", - }, - ) - bid_data = { - "offer_id": offer["offer_id"], - "amount_from": bid_amount, - } - - if "address" in bid_template: - addr_from = bid_template["address"] - if addr_from != -1 and addr_from != "auto": - bid_data["addr_from"] = addr_from - - if config.get("test_mode", False): - print("Would create bid: {}".format(bid_data)) - bid_id = "simulated" - else: - if args.debug: - print("Creating bid: {}".format(bid_data)) - new_bid = read_json_api("bids/new", bid_data) - if "error" in new_bid: - raise ValueError( - "Server failed to create bid: {}".format( - new_bid["error"] - ) - ) - print( - "New bid: {} on offer {}".format( - new_bid["bid_id"], offer["offer_id"] - ) - ) - bid_id = new_bid["bid_id"] - - script_state["bids"][template_name].append( - {"bid_id": bid_id, "time": int(time.time()), "active": True} - ) - - max_seconds_between_bids = config["max_seconds_between_bids"] - min_seconds_between_bids = config["min_seconds_between_bids"] - if max_seconds_between_bids > min_seconds_between_bids: - time_between_bids = random.randint( - min_seconds_between_bids, max_seconds_between_bids - ) - else: - time_between_bids = min_seconds_between_bids - script_state["delay_next_bid_before"] = ( - int(time.time()) + time_between_bids - ) - write_state(args.statefile, script_state) - break # Create max one bid per iteration + now = int(time.time()) + prune_state_delay = config["prune_state_delay"] + if prune_state_delay > 0: + if ( + now - script_state.get("time_last_pruned_state", 0) + > prune_state_delay + ): + prune_script_state(now, args, config, script_state) except Exception as e: print(f"Error: {e}.") + if args.debug: + traceback.print_exc() if args.oneshot: break print("Looping indefinitely, ctrl+c to exit.") - delay_event.wait(60) + delay_event.wait(config["main_loop_delay"]) print("Done.")