mango-explorer/bin/liquidator

140 lines
6.2 KiB
Plaintext
Raw Normal View History

#!/usr/bin/env pyston3
import os, sys
from pathlib import Path
# Get the full path to this script.
script_path = Path(os.path.realpath(__file__))
# The parent of the script is the bin directory.
# The parent of the bin directory is the notebook directory.
# It's this notebook directory we want.
notebook_directory = script_path.parent.parent
# Add the notebook directory to our import path.
sys.path.append(str(notebook_directory))
# Add the startup directory to our import path.
startup_directory = notebook_directory / "meta" / "startup"
sys.path.append(str(startup_directory))
import argparse
import logging
import os.path
import projectsetup
import time
import traceback
from decimal import Decimal
from AccountScout import AccountScout
from AccountLiquidator import AccountLiquidator, ForceCancelOrdersAccountLiquidator, NullAccountLiquidator
from BaseModel import Group
from Constants import WARNING_DISCLAIMER_TEXT
from Context import Context, default_cluster, default_cluster_url, default_program_id, default_dex_program_id, default_group_name, default_group_id
from SimpleLiquidator import SimpleLiquidator
from TradeExecutor import SerumImmediateTradeExecutor
from Wallet import Wallet
from WalletBalancer import LiveWalletBalancer, NullWalletBalancer, TargetBalanceParser
# We explicitly want argument parsing to be outside the main try-except block because some arguments
# (like --help) will cause an exit, which our except: block traps.
parser = argparse.ArgumentParser(description="Run a liquidator for a Mango Markets group.")
parser.add_argument("--cluster", type=str, default=default_cluster,
help="Solana RPC cluster name")
parser.add_argument("--cluster-url", type=str, default=default_cluster_url,
help="Solana RPC cluster URL")
parser.add_argument("--program-id", type=str, default=default_program_id,
help="Mango program ID/address")
parser.add_argument("--dex-program-id", type=str, default=default_dex_program_id,
help="DEX program ID/address")
parser.add_argument("--group-name", type=str, default=default_group_name,
help="Mango group name")
parser.add_argument("--group-id", type=str, default=default_group_id,
help="Mango group ID/address")
parser.add_argument("--id-file", type=str, default="id.json",
help="file containing the JSON-formatted wallet private key")
parser.add_argument("--log-level", default=logging.INFO, type=lambda level: getattr(logging, level),
help="level of verbosity to log (possible values: DEBUG, INFO, WARNING, ERROR, CRITICAL)")
parser.add_argument("--throttle-to-seconds", type=int, default=60,
help="minimum number of seconds between each loop (including time taken processing accounts)")
parser.add_argument("--target", type=str, action="append",
help="token symbol plus target value or percentage, separated by a colon (e.g. 'ETH:2.5' or 'ETH:33%')")
parser.add_argument("--action-threshold", type=Decimal, default=Decimal("0.01"),
help="fraction of total wallet value a trade must be above to be carried out")
parser.add_argument("--adjustment-factor", type=Decimal, default=Decimal("0.05"),
help="factor by which to adjust the SELL price (akin to maximum slippage)")
parser.add_argument("--dry-run", action="store_true", default=False,
help="runs as read-only and does not perform any transactions")
args = parser.parse_args()
logging.getLogger().setLevel(args.log_level)
logging.warning(WARNING_DISCLAIMER_TEXT)
try:
id_filename = args.id_file
if not os.path.isfile(id_filename):
logging.error(f"Wallet file '{id_filename}' is not present.")
sys.exit(1)
wallet = Wallet.load(id_filename)
action_threshold = args.action_threshold
adjustment_factor = args.adjustment_factor
throttle_to_seconds = args.throttle_to_seconds
context = Context(args.cluster, args.cluster_url, args.program_id, args.dex_program_id, args.group_name,
args.group_id)
logging.info(f"Context: {context}")
logging.info(f"Wallet address: {wallet.address}")
group = Group.load(context)
logging.info("Checking wallet accounts.")
scout = AccountScout()
report = scout.verify_account_prepared_for_group(context, group, wallet.address)
logging.info(f"Wallet account report: {report}")
if report.has_errors:
raise Exception(f"Account '{wallet.address}' is not prepared for group '{group.address}'.")
logging.info("Wallet accounts OK.")
if args.dry_run:
account_liquidator: AccountLiquidator = NullAccountLiquidator()
else:
account_liquidator = ForceCancelOrdersAccountLiquidator(context, wallet)
if args.dry_run or (args.target is None) or (len(args.target) == 0):
wallet_balancer = NullWalletBalancer()
else:
balance_parser = TargetBalanceParser(group.tokens)
targets = list(map(balance_parser.parse, args.target))
trade_executor = SerumImmediateTradeExecutor(context, wallet, group, adjustment_factor)
wallet_balancer = LiveWalletBalancer(context, wallet, trade_executor, action_threshold, group.tokens, targets)
stop = False
liquidator = SimpleLiquidator(context, wallet, account_liquidator, wallet_balancer)
while not stop:
started_at = time.time()
try:
liquidator.run()
time_taken = time.time() - started_at
should_sleep_for = throttle_to_seconds - int(time_taken)
sleep_for = max(should_sleep_for, 0)
logging.info(f"Check of all margin accounts complete. Time taken: {time_taken:.2f} seconds, sleeping for {sleep_for} seconds...")
time.sleep(sleep_for)
except KeyboardInterrupt:
stop = True
logging.info("Stopping...")
except Exception as exception:
logging.critical(f"Iteration failed because of exception: {exception}")
except Exception as exception:
logging.critical(f"Liquidator stopped because of exception: {exception} - {traceback.format_exc()}")
except:
logging.critical(f"Liquidator stopped because of uncatchable error: {traceback.format_exc()}")
finally:
logging.info("Liquidator completed.")