Big Rename - removal of old Group and MarginAccount, deletion of a lot of code that is no longer relevant.
* Added some TODO comments where work needs to be done but pieces are currently missing.
This commit is contained in:
parent
b8dc12a3e6
commit
5b71ffbd18
1584
Layouts.ipynb
1584
Layouts.ipynb
File diff suppressed because it is too large
Load Diff
4
Makefile
4
Makefile
|
@ -21,11 +21,11 @@ mypy:
|
||||||
for file in bin/* ; do \
|
for file in bin/* ; do \
|
||||||
cp $${file} .tmplintdir/$${file##*/}.py ; \
|
cp $${file} .tmplintdir/$${file##*/}.py ; \
|
||||||
done
|
done
|
||||||
-mypy mango tests .tmplintdir
|
-mypy --no-incremental --cache-dir=/dev/null mango tests .tmplintdir
|
||||||
rm -rf .tmplintdir
|
rm -rf .tmplintdir
|
||||||
|
|
||||||
flake8:
|
flake8:
|
||||||
flake8 --extend-ignore E402,E501,E722,W291,W391 . tests/* bin/*
|
flake8 --extend-ignore E402,E501,E722,W291,W391 . bin/*
|
||||||
|
|
||||||
lint: flake8 mypy
|
lint: flake8 mypy
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ if (token_account is None) or (token_account.value.token.mint != wrapped_sol.min
|
||||||
raise Exception(f"Account {args.address} is not a {wrapped_sol.name} account.")
|
raise Exception(f"Account {args.address} is not a {wrapped_sol.name} account.")
|
||||||
|
|
||||||
transaction = Transaction()
|
transaction = Transaction()
|
||||||
signers: typing.List[Account] = [wallet.account]
|
signers: typing.Sequence[Account] = [wallet.account]
|
||||||
payer = wallet.address
|
payer = wallet.address
|
||||||
|
|
||||||
close_instruction = mango.CloseSplAccountInstructionBuilder(context, wallet, args.address)
|
close_instruction = mango.CloseSplAccountInstructionBuilder(context, wallet, args.address)
|
||||||
|
|
|
@ -6,6 +6,7 @@ import os
|
||||||
import os.path
|
import os.path
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
import typing
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
@ -42,13 +43,15 @@ try:
|
||||||
logging.info(f"Wallet address: {wallet.address}")
|
logging.info(f"Wallet address: {wallet.address}")
|
||||||
|
|
||||||
group = mango.Group.load(context)
|
group = mango.Group.load(context)
|
||||||
tokens = [basket_token.token for basket_token in group.basket_tokens]
|
tokens = [token_info.token for token_info in group.tokens if token_info is not None]
|
||||||
|
|
||||||
balance_parser = mango.TargetBalanceParser(tokens)
|
balance_parser = mango.TargetBalanceParser(tokens)
|
||||||
targets = list(map(balance_parser.parse, args.target))
|
targets = list(map(balance_parser.parse, args.target))
|
||||||
logging.info(f"Targets: {targets}")
|
logging.info(f"Targets: {targets}")
|
||||||
|
|
||||||
prices = group.fetch_token_prices(context)
|
# TODO - fetch prices when available for V3.
|
||||||
|
# prices = group.fetch_token_prices(context)
|
||||||
|
prices: typing.Sequence[mango.TokenValue] = []
|
||||||
logging.info(f"Prices: {prices}")
|
logging.info(f"Prices: {prices}")
|
||||||
|
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
|
|
|
@ -36,7 +36,7 @@ try:
|
||||||
logging.info(f"Context: {context}")
|
logging.info(f"Context: {context}")
|
||||||
logging.info(f"Address: {address}")
|
logging.info(f"Address: {address}")
|
||||||
|
|
||||||
group = mango.MangoGroup.load(context)
|
group = mango.Group.load(context)
|
||||||
balances = group.fetch_balances(context, address)
|
balances = group.fetch_balances(context, address)
|
||||||
print("Balances:")
|
print("Balances:")
|
||||||
mango.TokenValue.report(print, balances)
|
mango.TokenValue.report(print, balances)
|
||||||
|
|
|
@ -24,12 +24,12 @@ logging.getLogger().setLevel(args.log_level)
|
||||||
context = mango.Context.from_command_line_parameters(args).new_from_cluster("devnet")
|
context = mango.Context.from_command_line_parameters(args).new_from_cluster("devnet")
|
||||||
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
|
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
|
||||||
|
|
||||||
group = mango.MangoGroup.load(context, context.group_id)
|
group = mango.Group.load(context, context.group_id)
|
||||||
|
|
||||||
mango_account = Account()
|
mango_account = Account()
|
||||||
init = mango.build_create_margin_account_instructions(context, wallet, group, mango_account)
|
init = mango.build_create_account_instructions(context, wallet, group, mango_account)
|
||||||
|
|
||||||
signers: typing.List[Account] = [wallet.account, mango_account]
|
signers: typing.Sequence[Account] = [wallet.account, mango_account]
|
||||||
|
|
||||||
transaction = Transaction()
|
transaction = Transaction()
|
||||||
transaction.instructions.extend(init)
|
transaction.instructions.extend(init)
|
|
@ -78,20 +78,17 @@ try:
|
||||||
item, mango.LiquidationEvent) and not item.succeeded)
|
item, mango.LiquidationEvent) and not item.succeeded)
|
||||||
liquidations_publisher.subscribe(on_next=filtering.send)
|
liquidations_publisher.subscribe(on_next=filtering.send)
|
||||||
|
|
||||||
|
# TODO: Add proper liquidator classes here when they're written for V3
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
account_liquidator: mango.AccountLiquidator = mango.NullAccountLiquidator()
|
account_liquidator: mango.AccountLiquidator = mango.NullAccountLiquidator()
|
||||||
else:
|
else:
|
||||||
intermediate = mango.ForceCancelOrdersAccountLiquidator(context, wallet)
|
account_liquidator = mango.NullAccountLiquidator()
|
||||||
account_liquidator = mango.ReportingAccountLiquidator(intermediate,
|
|
||||||
context,
|
|
||||||
wallet,
|
|
||||||
liquidations_publisher,
|
|
||||||
liquidator_name)
|
|
||||||
|
|
||||||
prices = group.fetch_token_prices(context)
|
# TODO - fetch prices when available for V3.
|
||||||
margin_account = mango.MarginAccount.load(context, margin_account_address, group)
|
# prices = group.fetch_token_prices(context)
|
||||||
|
margin_account = mango.Account.load(context, margin_account_address)
|
||||||
worthwhile_threshold = Decimal(0) # No threshold - don't take this into account.
|
worthwhile_threshold = Decimal(0) # No threshold - don't take this into account.
|
||||||
liquidatable_report = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
liquidatable_report = mango.LiquidatableReport.build(group, [], margin_account, worthwhile_threshold)
|
||||||
transaction_id = account_liquidator.liquidate(liquidatable_report)
|
transaction_id = account_liquidator.liquidate(liquidatable_report)
|
||||||
if transaction_id is None:
|
if transaction_id is None:
|
||||||
print("No transaction sent.")
|
print("No transaction sent.")
|
||||||
|
|
|
@ -96,7 +96,7 @@ try:
|
||||||
logging.info(f"Wallet address: {wallet.address}")
|
logging.info(f"Wallet address: {wallet.address}")
|
||||||
|
|
||||||
group = mango.Group.load(context)
|
group = mango.Group.load(context)
|
||||||
tokens = [basket_token.token for basket_token in group.basket_tokens]
|
tokens = [token_info.token for token_info in group.tokens if token_info is not None]
|
||||||
|
|
||||||
logging.info("Checking wallet accounts.")
|
logging.info("Checking wallet accounts.")
|
||||||
scout = mango.AccountScout()
|
scout = mango.AccountScout()
|
||||||
|
@ -123,16 +123,11 @@ try:
|
||||||
captured_failed_notification_target, lambda item: isinstance(item, mango.LiquidationEvent) and not item.succeeded)
|
captured_failed_notification_target, lambda item: isinstance(item, mango.LiquidationEvent) and not item.succeeded)
|
||||||
liquidations_publisher.subscribe(on_next=filtering_failed.send)
|
liquidations_publisher.subscribe(on_next=filtering_failed.send)
|
||||||
|
|
||||||
|
# TODO: Add proper liquidator classes here when they're written for V3
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
intermediate: mango.AccountLiquidator = mango.NullAccountLiquidator()
|
account_liquidator: mango.AccountLiquidator = mango.NullAccountLiquidator()
|
||||||
else:
|
else:
|
||||||
intermediate = mango.ForceCancelOrdersAccountLiquidator(context, wallet)
|
account_liquidator = mango.NullAccountLiquidator()
|
||||||
|
|
||||||
account_liquidator: mango.AccountLiquidator = mango.ReportingAccountLiquidator(intermediate,
|
|
||||||
context,
|
|
||||||
wallet,
|
|
||||||
liquidations_publisher,
|
|
||||||
liquidator_name)
|
|
||||||
|
|
||||||
if args.dry_run or (args.target is None) or (len(args.target) == 0):
|
if args.dry_run or (args.target is None) or (len(args.target) == 0):
|
||||||
wallet_balancer: mango.WalletBalancer = mango.NullWalletBalancer()
|
wallet_balancer: mango.WalletBalancer = mango.NullWalletBalancer()
|
||||||
|
@ -145,23 +140,16 @@ try:
|
||||||
|
|
||||||
# These (along with `context`) are captured and read by `load_updated_price_details()`.
|
# These (along with `context`) are captured and read by `load_updated_price_details()`.
|
||||||
group_address = group.address
|
group_address = group.address
|
||||||
oracle_addresses = list([market.oracle for market in group.markets])
|
oracle_addresses = group.oracles
|
||||||
|
|
||||||
def load_updated_price_details() -> typing.Tuple[mango.Group, typing.List[mango.TokenValue]]:
|
def load_updated_price_details() -> typing.Tuple[mango.Group, typing.Sequence[mango.TokenValue]]:
|
||||||
all_addresses = [group_address, *oracle_addresses]
|
all_addresses = [group_address, *oracle_addresses]
|
||||||
all_account_infos = mango.AccountInfo.load_multiple(context, all_addresses)
|
all_account_infos = mango.AccountInfo.load_multiple(context, all_addresses)
|
||||||
group_account_info = all_account_infos[0]
|
group_account_info = all_account_infos[0]
|
||||||
oracle_account_infos = all_account_infos[1:]
|
|
||||||
group = mango.Group.parse(context, group_account_info)
|
group = mango.Group.parse(context, group_account_info)
|
||||||
|
|
||||||
oracles = map(lambda oracle_account_info: mango.Aggregator.parse(
|
# TODO - fetch prices when code available in V3.
|
||||||
context, oracle_account_info), oracle_account_infos)
|
return group, []
|
||||||
prices = list(map(lambda oracle: oracle.price, oracles)) + [Decimal(1)]
|
|
||||||
token_prices = []
|
|
||||||
for index, price in enumerate(prices):
|
|
||||||
token_prices += [mango.TokenValue(group.basket_tokens[index].token, price)]
|
|
||||||
|
|
||||||
return group, token_prices
|
|
||||||
|
|
||||||
def fetch_prices(context):
|
def fetch_prices(context):
|
||||||
def _fetch_prices(_):
|
def _fetch_prices(_):
|
||||||
|
@ -175,7 +163,7 @@ try:
|
||||||
def fetch_margin_accounts(context):
|
def fetch_margin_accounts(context):
|
||||||
def _actual_fetch():
|
def _actual_fetch():
|
||||||
group = mango.Group.load(context)
|
group = mango.Group.load(context)
|
||||||
return mango.MarginAccount.load_ripe(context, group)
|
return mango.Account.load_ripe(context, group)
|
||||||
|
|
||||||
def _fetch_margin_accounts(_):
|
def _fetch_margin_accounts(_):
|
||||||
with mango.retry_context("Margin Account Fetch",
|
with mango.retry_context("Margin Account Fetch",
|
||||||
|
|
|
@ -73,27 +73,23 @@ try:
|
||||||
item, mango.LiquidationEvent) and not item.succeeded)
|
item, mango.LiquidationEvent) and not item.succeeded)
|
||||||
liquidations_publisher.subscribe(on_next=filtering.send)
|
liquidations_publisher.subscribe(on_next=filtering.send)
|
||||||
|
|
||||||
|
# TODO: Add proper liquidator classes here when they're written for V3
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
account_liquidator: mango.AccountLiquidator = mango.NullAccountLiquidator()
|
account_liquidator: mango.AccountLiquidator = mango.NullAccountLiquidator()
|
||||||
else:
|
else:
|
||||||
intermediate = mango.ForceCancelOrdersAccountLiquidator(context, wallet)
|
account_liquidator = mango.NullAccountLiquidator()
|
||||||
account_liquidator = mango.ReportingAccountLiquidator(intermediate,
|
|
||||||
context,
|
|
||||||
wallet,
|
|
||||||
liquidations_publisher,
|
|
||||||
liquidator_name)
|
|
||||||
|
|
||||||
wallet_balancer = mango.NullWalletBalancer()
|
wallet_balancer = mango.NullWalletBalancer()
|
||||||
|
|
||||||
liquidation_processor = mango.LiquidationProcessor(context, liquidator_name, account_liquidator, wallet_balancer)
|
liquidation_processor = mango.LiquidationProcessor(context, liquidator_name, account_liquidator, wallet_balancer)
|
||||||
|
|
||||||
started_at = time.time()
|
started_at = time.time()
|
||||||
ripe = group.load_ripe_margin_accounts()
|
# ripe = group.load_ripe_margin_accounts()
|
||||||
liquidation_processor.update_margin_accounts(ripe)
|
liquidation_processor.update_margin_accounts([])
|
||||||
|
|
||||||
group = mango.Group.load(context) # Refresh group data
|
group = mango.Group.load(context) # Refresh group data
|
||||||
prices = group.fetch_token_prices(context)
|
# prices = group.fetch_token_prices(context)
|
||||||
liquidation_processor.update_prices(group, prices)
|
liquidation_processor.update_prices(group, [])
|
||||||
|
|
||||||
time_taken = time.time() - started_at
|
time_taken = time.time() - started_at
|
||||||
logging.info(f"Check of all margin accounts complete. Time taken: {time_taken:.2f} seconds.")
|
logging.info(f"Check of all margin accounts complete. Time taken: {time_taken:.2f} seconds.")
|
||||||
|
|
|
@ -22,7 +22,7 @@ import mango # nopep8
|
||||||
parser = argparse.ArgumentParser(description="Sends an SPL tokens to a different address.")
|
parser = argparse.ArgumentParser(description="Sends an SPL tokens to a different address.")
|
||||||
mango.Context.add_command_line_parameters(parser)
|
mango.Context.add_command_line_parameters(parser)
|
||||||
mango.Wallet.add_command_line_parameters(parser)
|
mango.Wallet.add_command_line_parameters(parser)
|
||||||
parser.add_argument("--token-symbol", type=str, required=True, help="token symbol to send (e.g. ETH)")
|
parser.add_argument("--symbol", type=str, required=True, help="token symbol to send (e.g. ETH)")
|
||||||
parser.add_argument("--address", type=PublicKey,
|
parser.add_argument("--address", type=PublicKey,
|
||||||
help="Destination address for the SPL token - can be either the actual token address or the address of the owner of the token address")
|
help="Destination address for the SPL token - can be either the actual token address or the address of the owner of the token address")
|
||||||
parser.add_argument("--quantity", type=Decimal, required=True, help="quantity of token to send")
|
parser.add_argument("--quantity", type=Decimal, required=True, help="quantity of token to send")
|
||||||
|
@ -40,18 +40,18 @@ try:
|
||||||
logging.info(f"Context: {context}")
|
logging.info(f"Context: {context}")
|
||||||
logging.info(f"Wallet address: {wallet.address}")
|
logging.info(f"Wallet address: {wallet.address}")
|
||||||
|
|
||||||
group = mango.Group.load(context)
|
token = context.token_lookup.find_by_symbol(args.symbol)
|
||||||
group_basket_token = mango.BasketToken.find_by_symbol(group.basket_tokens, args.token_symbol)
|
if token is None:
|
||||||
group_token = group_basket_token.token
|
raise Exception(f"Could not find details of token with symbol {args.symbol}.")
|
||||||
|
|
||||||
spl_token = Token(context.client, group_token.mint, TOKEN_PROGRAM_ID, wallet.account)
|
spl_token = Token(context.client, token.mint, TOKEN_PROGRAM_ID, wallet.account)
|
||||||
source_accounts = spl_token.get_accounts(wallet.address)
|
source_accounts = spl_token.get_accounts(wallet.address)
|
||||||
source_account = source_accounts["result"]["value"][0]
|
source_account = source_accounts["result"]["value"][0]
|
||||||
source = PublicKey(source_account["pubkey"])
|
source = PublicKey(source_account["pubkey"])
|
||||||
|
|
||||||
# Is the address an actual token account? Or is it the SOL address of the owner?
|
# Is the address an actual token account? Or is it the SOL address of the owner?
|
||||||
possible_dest: typing.Optional[mango.TokenAccount] = mango.TokenAccount.load(context, args.address)
|
possible_dest: typing.Optional[mango.TokenAccount] = mango.TokenAccount.load(context, args.address)
|
||||||
if (possible_dest is not None) and (possible_dest.value.token.mint == group_token.mint):
|
if (possible_dest is not None) and (possible_dest.value.token.mint == token.mint):
|
||||||
# We successfully loaded the token account.
|
# We successfully loaded the token account.
|
||||||
destination: PublicKey = args.address
|
destination: PublicKey = args.address
|
||||||
else:
|
else:
|
||||||
|
@ -63,11 +63,11 @@ try:
|
||||||
destination = PublicKey(destination_account["pubkey"])
|
destination = PublicKey(destination_account["pubkey"])
|
||||||
|
|
||||||
owner = wallet.account
|
owner = wallet.account
|
||||||
amount = int(args.quantity * Decimal(10 ** group_token.decimals))
|
amount = int(args.quantity * Decimal(10 ** token.decimals))
|
||||||
|
|
||||||
print("Balance:", source_account["account"]["data"]["parsed"]
|
print("Balance:", source_account["account"]["data"]["parsed"]
|
||||||
["info"]["tokenAmount"]["uiAmountString"], group_token.name)
|
["info"]["tokenAmount"]["uiAmountString"], token.name)
|
||||||
text_amount = f"{amount} {group_token.name} (@ {group_token.decimals} decimal places)"
|
text_amount = f"{amount} {token.name} (@ {token.decimals} decimal places)"
|
||||||
print(f"Sending {text_amount}")
|
print(f"Sending {text_amount}")
|
||||||
print(f" From: {source}")
|
print(f" From: {source}")
|
||||||
print(f" To: {destination}")
|
print(f" To: {destination}")
|
||||||
|
@ -82,7 +82,7 @@ try:
|
||||||
|
|
||||||
updated_balance = spl_token.get_balance(source)
|
updated_balance = spl_token.get_balance(source)
|
||||||
updated_balance_text = updated_balance["result"]["value"]["uiAmountString"]
|
updated_balance_text = updated_balance["result"]["value"]["uiAmountString"]
|
||||||
print(f"{text_amount} sent. Balance now: {updated_balance_text} {group_token.name}")
|
print(f"{text_amount} sent. Balance now: {updated_balance_text} {token.name}")
|
||||||
except Exception as exception:
|
except Exception as exception:
|
||||||
logging.critical(f"send-token stopped because of exception: {exception} - {traceback.format_exc()}")
|
logging.critical(f"send-token stopped because of exception: {exception} - {traceback.format_exc()}")
|
||||||
except:
|
except:
|
||||||
|
|
|
@ -30,6 +30,6 @@ if address is None:
|
||||||
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
|
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
|
||||||
address = wallet.address
|
address = wallet.address
|
||||||
|
|
||||||
group = mango.MangoGroup.load(context, context.group_id)
|
group = mango.Group.load(context, context.group_id)
|
||||||
mango_accounts = mango.MangoAccount.load_all_for_owner(context, address, group)
|
mango_accounts = mango.Account.load_all_for_owner(context, address, group)
|
||||||
print(mango_accounts)
|
print(mango_accounts)
|
|
@ -11,8 +11,6 @@ sys.path.insert(0, os.path.abspath(
|
||||||
os.path.join(os.path.dirname(__file__), "..")))
|
os.path.join(os.path.dirname(__file__), "..")))
|
||||||
import mango # nopep8
|
import mango # nopep8
|
||||||
|
|
||||||
# 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="Shows the on-chain data of a Mango Markets Group.")
|
parser = argparse.ArgumentParser(description="Shows the on-chain data of a Mango Markets Group.")
|
||||||
mango.Context.add_command_line_parameters(parser)
|
mango.Context.add_command_line_parameters(parser)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
@ -23,7 +21,7 @@ logging.warning(mango.WARNING_DISCLAIMER_TEXT)
|
||||||
try:
|
try:
|
||||||
context = mango.Context.from_command_line_parameters(args)
|
context = mango.Context.from_command_line_parameters(args)
|
||||||
|
|
||||||
group = mango.MangoGroup.load(context)
|
group = mango.Group.load(context)
|
||||||
print(group)
|
print(group)
|
||||||
except Exception as exception:
|
except Exception as exception:
|
||||||
logging.critical(f"show-group stopped because of exception: {exception} - {traceback.format_exc()}")
|
logging.critical(f"show-group stopped because of exception: {exception} - {traceback.format_exc()}")
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
#!/usr/bin/env pyston3
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import os.path
|
|
||||||
import sys
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from solana.publickey import PublicKey
|
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath(
|
|
||||||
os.path.join(os.path.dirname(__file__), "..")))
|
|
||||||
import mango # nopep8
|
|
||||||
|
|
||||||
# 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="Shows the on-chain data of a Mango Markets Margin Account.")
|
|
||||||
mango.Context.add_command_line_parameters(parser)
|
|
||||||
parser.add_argument("--address", type=PublicKey, required=True,
|
|
||||||
help="Solana address of the Mango Markets Margin Account")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
logging.getLogger().setLevel(args.log_level)
|
|
||||||
logging.warning(mango.WARNING_DISCLAIMER_TEXT)
|
|
||||||
|
|
||||||
try:
|
|
||||||
context = mango.Context.from_command_line_parameters(args)
|
|
||||||
|
|
||||||
group = mango.Group.load(context)
|
|
||||||
margin_account = mango.MarginAccount.load(context, args.address, group)
|
|
||||||
|
|
||||||
print(margin_account)
|
|
||||||
except Exception as exception:
|
|
||||||
logging.critical(f"show-margin-account stopped because of exception: {exception} - {traceback.format_exc()}")
|
|
||||||
except:
|
|
||||||
logging.critical(f"show-margin-account stopped because of uncatchable error: {traceback.format_exc()}")
|
|
|
@ -34,10 +34,8 @@ if largest_token_account is None:
|
||||||
raise Exception(f"No {wrapped_sol.name} accounts found for owner {wallet.address}.")
|
raise Exception(f"No {wrapped_sol.name} accounts found for owner {wallet.address}.")
|
||||||
|
|
||||||
transaction = Transaction()
|
transaction = Transaction()
|
||||||
signers: typing.List[Account] = [wallet.account]
|
|
||||||
|
|
||||||
wrapped_sol_account = Account()
|
wrapped_sol_account = Account()
|
||||||
signers.append(wrapped_sol_account)
|
signers: typing.Sequence[Account] = [wallet.account, wrapped_sol_account]
|
||||||
|
|
||||||
create_instruction = mango.CreateSplAccountInstructionBuilder(
|
create_instruction = mango.CreateSplAccountInstructionBuilder(
|
||||||
context, wallet, wrapped_sol_account.public_key())
|
context, wallet, wrapped_sol_account.public_key())
|
||||||
|
|
10
bin/withdraw
10
bin/withdraw
|
@ -29,11 +29,11 @@ logging.getLogger().setLevel(args.log_level)
|
||||||
context = mango.Context.from_command_line_parameters(args)
|
context = mango.Context.from_command_line_parameters(args)
|
||||||
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
|
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
|
||||||
|
|
||||||
group = mango.MangoGroup.load(context, context.group_id)
|
group = mango.Group.load(context, context.group_id)
|
||||||
margin_accounts = mango.MangoAccount.load_all_for_owner(context, wallet.address, group)
|
accounts = mango.Account.load_all_for_owner(context, wallet.address, group)
|
||||||
if len(margin_accounts) == 0:
|
if len(accounts) == 0:
|
||||||
raise Exception(f"Could not find any margin accounts for '{wallet.address}'.")
|
raise Exception(f"Could not find any margin accounts for '{wallet.address}'.")
|
||||||
margin_account = margin_accounts[0]
|
margin_account = accounts[0]
|
||||||
|
|
||||||
token = context.token_lookup.find_by_symbol(args.symbol)
|
token = context.token_lookup.find_by_symbol(args.symbol)
|
||||||
print(token)
|
print(token)
|
||||||
|
@ -58,7 +58,7 @@ node_bank = root_bank.pick_node_bank(context)
|
||||||
withdraw = mango.build_withdraw_instructions(
|
withdraw = mango.build_withdraw_instructions(
|
||||||
context, wallet, group, margin_account, root_bank, node_bank, withdrawal_token_account, args.allow_borrow)
|
context, wallet, group, margin_account, root_bank, node_bank, withdrawal_token_account, args.allow_borrow)
|
||||||
print(withdraw)
|
print(withdraw)
|
||||||
signers: typing.List[Account] = [wallet.account]
|
signers: typing.Sequence[Account] = [wallet.account]
|
||||||
|
|
||||||
transaction = Transaction()
|
transaction = Transaction()
|
||||||
transaction.instructions.extend(withdraw)
|
transaction.instructions.extend(withdraw)
|
||||||
|
|
|
@ -31,9 +31,8 @@ wrapped_sol = context.token_lookup.find_by_symbol_or_raise("SOL")
|
||||||
amount_to_transfer = int(args.quantity * mango.SOL_DECIMAL_DIVISOR)
|
amount_to_transfer = int(args.quantity * mango.SOL_DECIMAL_DIVISOR)
|
||||||
|
|
||||||
transaction = Transaction()
|
transaction = Transaction()
|
||||||
signers: typing.List[Account] = [wallet.account]
|
|
||||||
wrapped_sol_account = Account()
|
wrapped_sol_account = Account()
|
||||||
signers.append(wrapped_sol_account)
|
signers: typing.Sequence[Account] = [wallet.account, wrapped_sol_account]
|
||||||
|
|
||||||
create_instruction = mango.CreateSplAccountInstructionBuilder(
|
create_instruction = mango.CreateSplAccountInstructionBuilder(
|
||||||
context, wallet, wrapped_sol_account.public_key(), amount_to_transfer)
|
context, wallet, wrapped_sol_account.public_key(), amount_to_transfer)
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
|
from .account import Account
|
||||||
|
from .accountflags import AccountFlags
|
||||||
from .accountinfo import AccountInfo
|
from .accountinfo import AccountInfo
|
||||||
from .accountliquidator import AccountLiquidator, NullAccountLiquidator, ActualAccountLiquidator, ForceCancelOrdersAccountLiquidator, ReportingAccountLiquidator
|
from .accountliquidator import AccountLiquidator, NullAccountLiquidator
|
||||||
from .accountscout import ScoutReport, AccountScout
|
from .accountscout import ScoutReport, AccountScout
|
||||||
from .addressableaccount import AddressableAccount
|
from .addressableaccount import AddressableAccount
|
||||||
from .aggregator import AggregatorConfig, Round, Answer, Aggregator
|
|
||||||
from .balancesheet import BalanceSheet
|
from .balancesheet import BalanceSheet
|
||||||
from .baskettoken import BasketToken
|
|
||||||
from .constants import SYSTEM_PROGRAM_ADDRESS, SOL_MINT_ADDRESS, SOL_DECIMALS, SOL_DECIMAL_DIVISOR, WARNING_DISCLAIMER_TEXT, MangoConstants
|
from .constants import SYSTEM_PROGRAM_ADDRESS, SOL_MINT_ADDRESS, SOL_DECIMALS, SOL_DECIMAL_DIVISOR, WARNING_DISCLAIMER_TEXT, MangoConstants
|
||||||
from .context import Context, default_cluster, default_cluster_url, default_program_id, default_dex_program_id, default_group_name, default_group_id
|
from .context import Context, default_cluster, default_cluster_url, default_program_id, default_dex_program_id, default_group_name, default_group_id
|
||||||
from .createmarketoperations import create_market_operations
|
from .createmarketoperations import create_market_operations
|
||||||
|
@ -12,21 +12,15 @@ from .encoding import decode_binary, encode_binary, encode_key, encode_int
|
||||||
from .group import Group
|
from .group import Group
|
||||||
from .idsjsontokenlookup import IdsJsonTokenLookup
|
from .idsjsontokenlookup import IdsJsonTokenLookup
|
||||||
from .idsjsonmarketlookup import IdsJsonMarketLookup
|
from .idsjsonmarketlookup import IdsJsonMarketLookup
|
||||||
from .index import Index
|
from .instructions import InstructionBuilder, CreateSplAccountInstructionBuilder, InitializeSplAccountInstructionBuilder, TransferSplTokensInstructionBuilder, CloseSplAccountInstructionBuilder, CreateSerumOpenOrdersInstructionBuilder, NewOrderV3InstructionBuilder, ConsumeEventsInstructionBuilder, SettleInstructionBuilder
|
||||||
from .instructions import InstructionBuilder, ForceCancelOrdersInstructionBuilder, LiquidateInstructionBuilder, CreateSplAccountInstructionBuilder, InitializeSplAccountInstructionBuilder, TransferSplTokensInstructionBuilder, CloseSplAccountInstructionBuilder, CreateSerumOpenOrdersInstructionBuilder, NewOrderV3InstructionBuilder, ConsumeEventsInstructionBuilder, SettleInstructionBuilder
|
|
||||||
from .instructiontype import InstructionType
|
from .instructiontype import InstructionType
|
||||||
from .liquidatablereport import LiquidatableState, LiquidatableReport
|
from .liquidatablereport import LiquidatableState, LiquidatableReport
|
||||||
from .liquidationevent import LiquidationEvent
|
from .liquidationevent import LiquidationEvent
|
||||||
from .liquidationprocessor import LiquidationProcessor, LiquidationProcessorState
|
from .liquidationprocessor import LiquidationProcessor, LiquidationProcessorState
|
||||||
from .mangoaccount import MangoAccount
|
|
||||||
from .mangoaccountflags import MangoAccountFlags
|
|
||||||
from .marginaccount import MarginAccount
|
|
||||||
from .market import Market
|
from .market import Market
|
||||||
from .marketlookup import MarketLookup, CompoundMarketLookup
|
from .marketlookup import MarketLookup, CompoundMarketLookup
|
||||||
from .marketmetadata import MarketMetadata
|
|
||||||
from .marketoperations import MarketOperations, NullMarketOperations
|
from .marketoperations import MarketOperations, NullMarketOperations
|
||||||
from .mangogroup import MangoGroup
|
from .merpsinstructions import build_cancel_perp_order_instructions, build_create_account_instructions, build_place_perp_order_instructions, build_withdraw_instructions
|
||||||
from .merpsinstructions import build_cancel_perp_order_instructions, build_create_margin_account_instructions, build_place_perp_order_instructions, build_withdraw_instructions
|
|
||||||
from .metadata import Metadata
|
from .metadata import Metadata
|
||||||
from .notification import NotificationTarget, TelegramNotificationTarget, DiscordNotificationTarget, MailjetNotificationTarget, CsvFileNotificationTarget, FilteringNotificationTarget, NotificationHandler, parse_subscription_target
|
from .notification import NotificationTarget, TelegramNotificationTarget, DiscordNotificationTarget, MailjetNotificationTarget, CsvFileNotificationTarget, FilteringNotificationTarget, NotificationHandler, parse_subscription_target
|
||||||
from .observables import PrintingObserverSubscriber, TimestampedPrintingObserverSubscriber, CollectingObserverSubscriber, CaptureFirstItem, FunctionObserver, create_backpressure_skipping_observer, debug_print_item, log_subscription_error, observable_pipeline_error_reporter, EventSource
|
from .observables import PrintingObserverSubscriber, TimestampedPrintingObserverSubscriber, CollectingObserverSubscriber, CaptureFirstItem, FunctionObserver, create_backpressure_skipping_observer, debug_print_item, log_subscription_error, observable_pipeline_error_reporter, EventSource
|
||||||
|
@ -41,7 +35,6 @@ from .perpmarketinfo import PerpMarketInfo
|
||||||
from .perpmarketoperations import PerpMarketOperations
|
from .perpmarketoperations import PerpMarketOperations
|
||||||
from .retrier import RetryWithPauses, retry_context
|
from .retrier import RetryWithPauses, retry_context
|
||||||
from .rootbank import NodeBank, RootBank
|
from .rootbank import NodeBank, RootBank
|
||||||
from .serumaccountflags import SerumAccountFlags
|
|
||||||
from .serummarketlookup import SerumMarketLookup
|
from .serummarketlookup import SerumMarketLookup
|
||||||
from .serummarketoperations import SerumMarketOperations
|
from .serummarketoperations import SerumMarketOperations
|
||||||
from .spltokenlookup import SplTokenLookup
|
from .spltokenlookup import SplTokenLookup
|
||||||
|
|
|
@ -23,66 +23,66 @@ from .accountinfo import AccountInfo
|
||||||
from .addressableaccount import AddressableAccount
|
from .addressableaccount import AddressableAccount
|
||||||
from .context import Context
|
from .context import Context
|
||||||
from .encoding import encode_key
|
from .encoding import encode_key
|
||||||
|
from .group import Group
|
||||||
from .layouts import layouts
|
from .layouts import layouts
|
||||||
from .mangogroup import MangoGroup
|
|
||||||
from .metadata import Metadata
|
from .metadata import Metadata
|
||||||
from .version import Version
|
from .version import Version
|
||||||
|
|
||||||
# # 🥭 MangoAccount class
|
# # 🥭 Account class
|
||||||
#
|
#
|
||||||
# `MangoAccount` holds information about the account for a particular user/wallet for a particualr `MangoGroup`.
|
# `Account` holds information about the account for a particular user/wallet for a particualr `Group`.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
class MangoAccount(AddressableAccount):
|
class Account(AddressableAccount):
|
||||||
def __init__(self, account_info: AccountInfo, version: Version,
|
def __init__(self, account_info: AccountInfo, version: Version,
|
||||||
meta_data: Metadata, group: PublicKey, owner: PublicKey, in_basket: typing.List[Decimal],
|
meta_data: Metadata, group: PublicKey, owner: PublicKey, in_basket: typing.Sequence[Decimal],
|
||||||
deposits: typing.List[Decimal], borrows: typing.List[Decimal],
|
deposits: typing.Sequence[Decimal], borrows: typing.Sequence[Decimal],
|
||||||
spot_open_orders: typing.List[PublicKey], perp_accounts: typing.List[typing.Any]):
|
spot_open_orders: typing.Sequence[PublicKey], perp_accounts: typing.Sequence[typing.Any]):
|
||||||
super().__init__(account_info)
|
super().__init__(account_info)
|
||||||
self.version: Version = version
|
self.version: Version = version
|
||||||
|
|
||||||
self.meta_data: Metadata = meta_data
|
self.meta_data: Metadata = meta_data
|
||||||
self.group: PublicKey = group
|
self.group: PublicKey = group
|
||||||
self.owner: PublicKey = owner
|
self.owner: PublicKey = owner
|
||||||
self.in_basket: typing.List[Decimal] = in_basket
|
self.in_basket: typing.Sequence[Decimal] = in_basket
|
||||||
self.deposits: typing.List[Decimal] = deposits
|
self.deposits: typing.Sequence[Decimal] = deposits
|
||||||
self.borrows: typing.List[Decimal] = borrows
|
self.borrows: typing.Sequence[Decimal] = borrows
|
||||||
self.spot_open_orders: typing.List[PublicKey] = spot_open_orders
|
self.spot_open_orders: typing.Sequence[PublicKey] = spot_open_orders
|
||||||
self.perp_accounts: typing.List[layouts.PERP_ACCOUNT] = perp_accounts
|
self.perp_accounts: typing.Sequence[layouts.PERP_ACCOUNT] = perp_accounts
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_layout(layout: layouts.MANGO_ACCOUNT, account_info: AccountInfo, version: Version) -> "MangoAccount":
|
def from_layout(layout: layouts.MANGO_ACCOUNT, account_info: AccountInfo, version: Version) -> "Account":
|
||||||
meta_data = Metadata.from_layout(layout.meta_data)
|
meta_data = Metadata.from_layout(layout.meta_data)
|
||||||
group: PublicKey = layout.group
|
group: PublicKey = layout.group
|
||||||
owner: PublicKey = layout.owner
|
owner: PublicKey = layout.owner
|
||||||
in_basket: typing.List[Decimal] = layout.in_basket
|
in_basket: typing.Sequence[Decimal] = layout.in_basket
|
||||||
deposits: typing.List[Decimal] = layout.deposits
|
deposits: typing.Sequence[Decimal] = layout.deposits
|
||||||
borrows: typing.List[Decimal] = layout.borrows
|
borrows: typing.Sequence[Decimal] = layout.borrows
|
||||||
spot_open_orders: typing.List[PublicKey] = layout.spot_open_orders
|
spot_open_orders: typing.Sequence[PublicKey] = layout.spot_open_orders
|
||||||
perp_accounts: typing.List[typing.Any] = layout.perp_accounts
|
perp_accounts: typing.Sequence[typing.Any] = layout.perp_accounts
|
||||||
|
|
||||||
return MangoAccount(account_info, version, meta_data, group, owner, in_basket, deposits, borrows, spot_open_orders, perp_accounts)
|
return Account(account_info, version, meta_data, group, owner, in_basket, deposits, borrows, spot_open_orders, perp_accounts)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse(context: Context, account_info: AccountInfo) -> "MangoAccount":
|
def parse(context: Context, account_info: AccountInfo) -> "Account":
|
||||||
data = account_info.data
|
data = account_info.data
|
||||||
if len(data) != layouts.MANGO_ACCOUNT.sizeof():
|
if len(data) != layouts.MANGO_ACCOUNT.sizeof():
|
||||||
raise Exception(
|
raise Exception(
|
||||||
f"MangoAccount data length ({len(data)}) does not match expected size ({layouts.MANGO_ACCOUNT.sizeof()}")
|
f"Account data length ({len(data)}) does not match expected size ({layouts.MANGO_ACCOUNT.sizeof()}")
|
||||||
|
|
||||||
layout = layouts.MANGO_ACCOUNT.parse(data)
|
layout = layouts.MANGO_ACCOUNT.parse(data)
|
||||||
return MangoAccount.from_layout(layout, account_info, Version.V1)
|
return Account.from_layout(layout, account_info, Version.V1)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load(context: Context, address: PublicKey) -> "MangoAccount":
|
def load(context: Context, address: PublicKey) -> "Account":
|
||||||
account_info = AccountInfo.load(context, address)
|
account_info = AccountInfo.load(context, address)
|
||||||
if account_info is None:
|
if account_info is None:
|
||||||
raise Exception(f"MangoAccount account not found at address '{address}'")
|
raise Exception(f"Account account not found at address '{address}'")
|
||||||
return MangoAccount.parse(context, account_info)
|
return Account.parse(context, account_info)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load_all_for_owner(context: Context, owner: PublicKey, group: MangoGroup) -> typing.List["MangoAccount"]:
|
def load_all_for_owner(context: Context, owner: PublicKey, group: Group) -> typing.Sequence["Account"]:
|
||||||
# mango_group is just after the METADATA, which is the first entry.
|
# mango_group is just after the METADATA, which is the first entry.
|
||||||
group_offset = layouts.METADATA.sizeof()
|
group_offset = layouts.METADATA.sizeof()
|
||||||
# owner is just after mango_group in the layout, and it's a PublicKey which is 32 bytes.
|
# owner is just after mango_group in the layout, and it's a PublicKey which is 32 bytes.
|
||||||
|
@ -104,7 +104,7 @@ class MangoAccount(AddressableAccount):
|
||||||
for account_data in response["result"]:
|
for account_data in response["result"]:
|
||||||
address = PublicKey(account_data["pubkey"])
|
address = PublicKey(account_data["pubkey"])
|
||||||
account_info = AccountInfo._from_response_values(account_data["account"], address)
|
account_info = AccountInfo._from_response_values(account_data["account"], address)
|
||||||
account = MangoAccount.parse(context, account_info)
|
account = Account.parse(context, account_info)
|
||||||
accounts += [account]
|
accounts += [account]
|
||||||
return accounts
|
return accounts
|
||||||
|
|
|
@ -21,12 +21,12 @@ from .layouts import layouts
|
||||||
from .version import Version
|
from .version import Version
|
||||||
|
|
||||||
|
|
||||||
# # 🥭 SerumAccountFlags class
|
# # 🥭 AccountFlags class
|
||||||
#
|
#
|
||||||
# The Serum prefix is because there's also `MangoAccountFlags` for the Mango-specific flags.
|
# Encapsulates the Serum AccountFlags data.
|
||||||
#
|
#
|
||||||
|
|
||||||
class SerumAccountFlags:
|
class AccountFlags:
|
||||||
def __init__(self, version: Version, initialized: bool, market: bool, open_orders: bool,
|
def __init__(self, version: Version, initialized: bool, market: bool, open_orders: bool,
|
||||||
request_queue: bool, event_queue: bool, bids: bool, asks: bool, disabled: bool):
|
request_queue: bool, event_queue: bool, bids: bool, asks: bool, disabled: bool):
|
||||||
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
||||||
|
@ -41,10 +41,10 @@ class SerumAccountFlags:
|
||||||
self.disabled: bool = disabled
|
self.disabled: bool = disabled
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_layout(layout: layouts.SERUM_ACCOUNT_FLAGS) -> "SerumAccountFlags":
|
def from_layout(layout: layouts.ACCOUNT_FLAGS) -> "AccountFlags":
|
||||||
return SerumAccountFlags(Version.UNSPECIFIED, layout.initialized, layout.market,
|
return AccountFlags(Version.UNSPECIFIED, layout.initialized, layout.market,
|
||||||
layout.open_orders, layout.request_queue, layout.event_queue,
|
layout.open_orders, layout.request_queue, layout.event_queue,
|
||||||
layout.bids, layout.asks, layout.disabled)
|
layout.bids, layout.asks, layout.disabled)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
flags: typing.List[typing.Optional[str]] = []
|
flags: typing.List[typing.Optional[str]] = []
|
||||||
|
@ -57,7 +57,7 @@ class SerumAccountFlags:
|
||||||
flags += ["asks" if self.asks else None]
|
flags += ["asks" if self.asks else None]
|
||||||
flags += ["disabled" if self.disabled else None]
|
flags += ["disabled" if self.disabled else None]
|
||||||
flag_text = " | ".join(flag for flag in flags if flag is not None) or "None"
|
flag_text = " | ".join(flag for flag in flags if flag is not None) or "None"
|
||||||
return f"« SerumAccountFlags: {flag_text} »"
|
return f"« AccountFlags: {flag_text} »"
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"{self}"
|
return f"{self}"
|
|
@ -40,7 +40,7 @@ class AccountInfo:
|
||||||
self.rent_epoch: Decimal = rent_epoch
|
self.rent_epoch: Decimal = rent_epoch
|
||||||
self.data: bytes = data
|
self.data: bytes = data
|
||||||
|
|
||||||
def encoded_data(self) -> typing.List:
|
def encoded_data(self) -> typing.Sequence:
|
||||||
return encode_binary(self.data)
|
return encode_binary(self.data)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
|
@ -64,12 +64,12 @@ class AccountInfo:
|
||||||
return AccountInfo._from_response_values(result["value"], address)
|
return AccountInfo._from_response_values(result["value"], address)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load_multiple(context: Context, addresses: typing.Sequence[PublicKey], chunk_size: int = 100, sleep_between_calls: float = 0.0) -> typing.List["AccountInfo"]:
|
def load_multiple(context: Context, addresses: typing.Sequence[PublicKey], chunk_size: int = 100, sleep_between_calls: float = 0.0) -> typing.Sequence["AccountInfo"]:
|
||||||
# This is a tricky one to get right.
|
# This is a tricky one to get right.
|
||||||
# Some errors this can generate:
|
# Some errors this can generate:
|
||||||
# 413 Client Error: Payload Too Large for url
|
# 413 Client Error: Payload Too Large for url
|
||||||
# Error response from server: 'Too many inputs provided; max 100', code: -32602
|
# Error response from server: 'Too many inputs provided; max 100', code: -32602
|
||||||
address_strings: typing.List[str] = list(map(PublicKey.__str__, addresses))
|
address_strings: typing.Sequence[str] = list(map(PublicKey.__str__, addresses))
|
||||||
multiple: typing.List[AccountInfo] = []
|
multiple: typing.List[AccountInfo] = []
|
||||||
chunks = AccountInfo._split_list_into_chunks(address_strings, chunk_size)
|
chunks = AccountInfo._split_list_into_chunks(address_strings, chunk_size)
|
||||||
for counter, chunk in enumerate(chunks):
|
for counter, chunk in enumerate(chunks):
|
||||||
|
@ -100,7 +100,7 @@ class AccountInfo:
|
||||||
return AccountInfo._from_response_values(response["result"]["value"], address)
|
return AccountInfo._from_response_values(response["result"]["value"], address)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _split_list_into_chunks(to_chunk: typing.List, chunk_size: int = 100) -> typing.List[typing.List]:
|
def _split_list_into_chunks(to_chunk: typing.Sequence, chunk_size: int = 100) -> typing.Sequence[typing.Sequence]:
|
||||||
chunks = []
|
chunks = []
|
||||||
start = 0
|
start = 0
|
||||||
while start < len(to_chunk):
|
while start < len(to_chunk):
|
||||||
|
|
|
@ -15,27 +15,16 @@
|
||||||
|
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
import datetime
|
|
||||||
import logging
|
import logging
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from solana.transaction import Transaction
|
from .instructions import InstructionBuilder
|
||||||
|
|
||||||
from .context import Context
|
|
||||||
from .group import Group
|
|
||||||
from .instructions import ForceCancelOrdersInstructionBuilder, InstructionBuilder, LiquidateInstructionBuilder
|
|
||||||
from .liquidatablereport import LiquidatableReport
|
from .liquidatablereport import LiquidatableReport
|
||||||
from .liquidationevent import LiquidationEvent
|
|
||||||
from .marginaccount import MarginAccount
|
|
||||||
from .observables import EventSource
|
|
||||||
from .tokenvalue import TokenValue
|
|
||||||
from .transactionscout import TransactionScout
|
|
||||||
from .wallet import Wallet
|
|
||||||
|
|
||||||
|
|
||||||
# # 🥭 AccountLiquidator
|
# # 🥭 AccountLiquidator
|
||||||
#
|
#
|
||||||
# An `AccountLiquidator` liquidates a `MarginAccount`, if possible.
|
# An `AccountLiquidator` liquidates an `Account`, if possible.
|
||||||
#
|
#
|
||||||
# The follows the common pattern of having an abstract base class that defines the interface
|
# The follows the common pattern of having an abstract base class that defines the interface
|
||||||
# external code should use, along with a 'null' implementation and at least one full
|
# external code should use, along with a 'null' implementation and at least one full
|
||||||
|
@ -53,13 +42,12 @@ from .wallet import Wallet
|
||||||
# is just the `liquidate()` method.
|
# is just the `liquidate()` method.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
class AccountLiquidator(metaclass=abc.ABCMeta):
|
class AccountLiquidator(metaclass=abc.ABCMeta):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def prepare_instructions(self, liquidatable_report: LiquidatableReport) -> typing.List[InstructionBuilder]:
|
def prepare_instructions(self, liquidatable_report: LiquidatableReport) -> typing.Sequence[InstructionBuilder]:
|
||||||
raise NotImplementedError("AccountLiquidator.prepare_instructions() is not implemented on the base type.")
|
raise NotImplementedError("AccountLiquidator.prepare_instructions() is not implemented on the base type.")
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
|
@ -72,192 +60,13 @@ class AccountLiquidator(metaclass=abc.ABCMeta):
|
||||||
# A 'null', 'no-op', 'dry run' implementation of the `AccountLiquidator` class.
|
# A 'null', 'no-op', 'dry run' implementation of the `AccountLiquidator` class.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
class NullAccountLiquidator(AccountLiquidator):
|
class NullAccountLiquidator(AccountLiquidator):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def prepare_instructions(self, liquidatable_report: LiquidatableReport) -> typing.List[InstructionBuilder]:
|
def prepare_instructions(self, liquidatable_report: LiquidatableReport) -> typing.Sequence[InstructionBuilder]:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def liquidate(self, liquidatable_report: LiquidatableReport) -> typing.Optional[str]:
|
def liquidate(self, liquidatable_report: LiquidatableReport) -> typing.Optional[str]:
|
||||||
self.logger.info(f"Skipping liquidation of margin account [{liquidatable_report.margin_account.address}]")
|
self.logger.info(f"Skipping liquidation of account [{liquidatable_report.account.address}]")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
# # 💧 ActualAccountLiquidator class
|
|
||||||
#
|
|
||||||
# This full implementation takes a `MarginAccount` and liquidates it.
|
|
||||||
#
|
|
||||||
# It can also serve as a base class for further derivation. Derived classes may override
|
|
||||||
# `prepare_instructions()` to extend the liquidation process (for example to cancel
|
|
||||||
# outstanding orders before liquidating).
|
|
||||||
#
|
|
||||||
|
|
||||||
class ActualAccountLiquidator(AccountLiquidator):
|
|
||||||
def __init__(self, context: Context, wallet: Wallet):
|
|
||||||
super().__init__()
|
|
||||||
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
|
||||||
self.context = context
|
|
||||||
self.wallet = wallet
|
|
||||||
|
|
||||||
def prepare_instructions(self, liquidatable_report: LiquidatableReport) -> typing.List[InstructionBuilder]:
|
|
||||||
liquidate_instructions: typing.List[InstructionBuilder] = []
|
|
||||||
liquidate_instruction = LiquidateInstructionBuilder.from_margin_account_and_market(
|
|
||||||
self.context, liquidatable_report.group, self.wallet, liquidatable_report.margin_account, liquidatable_report.prices)
|
|
||||||
if liquidate_instruction is not None:
|
|
||||||
liquidate_instructions += [liquidate_instruction]
|
|
||||||
|
|
||||||
return liquidate_instructions
|
|
||||||
|
|
||||||
def liquidate(self, liquidatable_report: LiquidatableReport) -> typing.Optional[str]:
|
|
||||||
instruction_builders = self.prepare_instructions(liquidatable_report)
|
|
||||||
|
|
||||||
if len(instruction_builders) == 0:
|
|
||||||
return None
|
|
||||||
|
|
||||||
transaction = Transaction()
|
|
||||||
for builder in instruction_builders:
|
|
||||||
transaction.add(builder.build())
|
|
||||||
|
|
||||||
for instruction in transaction.instructions:
|
|
||||||
self.logger.debug("INSTRUCTION")
|
|
||||||
self.logger.debug(" Keys:")
|
|
||||||
for key in instruction.keys:
|
|
||||||
self.logger.debug(" ", f"{key.pubkey}".ljust(
|
|
||||||
45), f"{key.is_signer}".ljust(6), f"{key.is_writable}".ljust(6))
|
|
||||||
self.logger.debug(" Data:", " ".join(f"{x:02x}" for x in instruction.data))
|
|
||||||
self.logger.debug(" Program ID:", instruction.program_id)
|
|
||||||
|
|
||||||
transaction_response = self.context.client.send_transaction(
|
|
||||||
transaction, self.wallet.account, opts=self.context.transaction_options)
|
|
||||||
transaction_id = self.context.unwrap_transaction_id_or_raise_exception(transaction_response)
|
|
||||||
return transaction_id
|
|
||||||
|
|
||||||
|
|
||||||
# # 🌪️ ForceCancelOrdersAccountLiquidator class
|
|
||||||
#
|
|
||||||
# When liquidating an account, it's a good idea to ensure it has no open orders that could
|
|
||||||
# lock funds. This is why Mango allows a liquidator to force-close orders on a liquidatable
|
|
||||||
# account.
|
|
||||||
#
|
|
||||||
# `ForceCancelOrdersAccountLiquidator` overrides `prepare_instructions()` to inject any
|
|
||||||
# necessary force-cancel instructions before the `PartialLiquidate` instruction.
|
|
||||||
#
|
|
||||||
# This is not always necessary. For example, if the liquidator is partially-liquidating a
|
|
||||||
# large account, then perhaps only the first partial-liquidate needs to check and force-close
|
|
||||||
# orders, and subsequent partial liquidations can skip this step as an optimisation.
|
|
||||||
#
|
|
||||||
# The separation of the regular `AccountLiquidator` and the
|
|
||||||
# `ForceCancelOrdersAccountLiquidator` classes allows the caller to determine which process
|
|
||||||
# is used.
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
class ForceCancelOrdersAccountLiquidator(ActualAccountLiquidator):
|
|
||||||
def __init__(self, context: Context, wallet: Wallet):
|
|
||||||
super().__init__(context, wallet)
|
|
||||||
|
|
||||||
def prepare_instructions(self, liquidatable_report: LiquidatableReport) -> typing.List[InstructionBuilder]:
|
|
||||||
force_cancel_orders_instructions: typing.List[InstructionBuilder] = []
|
|
||||||
for index, market_metadata in enumerate(liquidatable_report.group.markets):
|
|
||||||
open_orders = liquidatable_report.margin_account.open_orders_accounts[index]
|
|
||||||
if open_orders is not None:
|
|
||||||
market = market_metadata.fetch_market(self.context)
|
|
||||||
orders = market.load_orders_for_owner(liquidatable_report.margin_account.owner)
|
|
||||||
order_count = len(orders)
|
|
||||||
if order_count > 0:
|
|
||||||
force_cancel_orders_instructions += ForceCancelOrdersInstructionBuilder.multiple_instructions_from_margin_account_and_market(
|
|
||||||
self.context, liquidatable_report.group, self.wallet, liquidatable_report.margin_account, market_metadata, order_count)
|
|
||||||
|
|
||||||
all_instructions = force_cancel_orders_instructions + super().prepare_instructions(liquidatable_report)
|
|
||||||
|
|
||||||
return all_instructions
|
|
||||||
|
|
||||||
|
|
||||||
# 📝 ReportingAccountLiquidator class
|
|
||||||
#
|
|
||||||
# This class takes a regular `AccountLiquidator` and wraps its `liquidate()` call in some
|
|
||||||
# useful reporting.
|
|
||||||
#
|
|
||||||
|
|
||||||
class ReportingAccountLiquidator(AccountLiquidator):
|
|
||||||
def __init__(self, inner: AccountLiquidator, context: Context, wallet: Wallet, liquidations_publisher: EventSource[LiquidationEvent], liquidator_name: str):
|
|
||||||
super().__init__()
|
|
||||||
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
|
||||||
self.inner: AccountLiquidator = inner
|
|
||||||
self.context: Context = context
|
|
||||||
self.wallet: Wallet = wallet
|
|
||||||
self.liquidations_publisher: EventSource[LiquidationEvent] = liquidations_publisher
|
|
||||||
self.liquidator_name: str = liquidator_name
|
|
||||||
|
|
||||||
def prepare_instructions(self, liquidatable_report: LiquidatableReport) -> typing.List[InstructionBuilder]:
|
|
||||||
return self.inner.prepare_instructions(liquidatable_report)
|
|
||||||
|
|
||||||
def liquidate(self, liquidatable_report: LiquidatableReport) -> typing.Optional[str]:
|
|
||||||
balances_before = liquidatable_report.group.fetch_balances(self.context, self.wallet.address)
|
|
||||||
self.logger.info("Wallet balances before:")
|
|
||||||
TokenValue.report(self.logger.info, balances_before)
|
|
||||||
|
|
||||||
self.logger.info(f"Margin account balances before:\n{liquidatable_report.balances}")
|
|
||||||
self.logger.info(
|
|
||||||
f"Liquidating margin account: {liquidatable_report.margin_account}\n{liquidatable_report.balance_sheet}")
|
|
||||||
try:
|
|
||||||
transaction_id = self.inner.liquidate(liquidatable_report)
|
|
||||||
except Exception as exception:
|
|
||||||
# It would be nice if we had a strongly-typed way of checking this.
|
|
||||||
if "MangoErrorCode::NotLiquidatable" in str(exception):
|
|
||||||
failed_liquidation_event = LiquidationEvent(datetime.datetime.now(),
|
|
||||||
self.liquidator_name,
|
|
||||||
self.context.group_name,
|
|
||||||
False,
|
|
||||||
"",
|
|
||||||
self.wallet.address,
|
|
||||||
liquidatable_report.margin_account.address,
|
|
||||||
balances_before,
|
|
||||||
balances_before)
|
|
||||||
self.liquidations_publisher.publish(failed_liquidation_event)
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
raise exception
|
|
||||||
|
|
||||||
if transaction_id is None:
|
|
||||||
self.logger.info("No transaction sent.")
|
|
||||||
else:
|
|
||||||
self.logger.info(f"Transaction ID: {transaction_id} - waiting for confirmation...")
|
|
||||||
|
|
||||||
response = self.context.wait_for_confirmation(transaction_id)
|
|
||||||
if response is None:
|
|
||||||
self.logger.warning(
|
|
||||||
f"Could not process 'after' liquidation stage - no data for transaction {transaction_id}")
|
|
||||||
return transaction_id
|
|
||||||
|
|
||||||
transaction_scout = TransactionScout.from_transaction_response(self.context, response)
|
|
||||||
|
|
||||||
group_after = Group.load(self.context)
|
|
||||||
margin_account_after_liquidation = MarginAccount.load(
|
|
||||||
self.context, liquidatable_report.margin_account.address, group_after)
|
|
||||||
intrinsic_balances_after = margin_account_after_liquidation.get_intrinsic_balances(group_after)
|
|
||||||
self.logger.info(f"Margin account balances after: {intrinsic_balances_after}")
|
|
||||||
|
|
||||||
self.logger.info("Wallet Balances After:")
|
|
||||||
balances_after = group_after.fetch_balances(self.context, self.wallet.address)
|
|
||||||
TokenValue.report(self.logger.info, balances_after)
|
|
||||||
|
|
||||||
liquidation_event = LiquidationEvent(datetime.datetime.now(),
|
|
||||||
self.liquidator_name,
|
|
||||||
self.context.group_name,
|
|
||||||
transaction_scout.succeeded,
|
|
||||||
transaction_id,
|
|
||||||
self.wallet.address,
|
|
||||||
margin_account_after_liquidation.address,
|
|
||||||
balances_before,
|
|
||||||
balances_after)
|
|
||||||
|
|
||||||
self.logger.info("Wallet Balances Changes:")
|
|
||||||
changes = TokenValue.changes(balances_before, balances_after)
|
|
||||||
TokenValue.report(self.logger.info, changes)
|
|
||||||
|
|
||||||
self.liquidations_publisher.publish(liquidation_event)
|
|
||||||
|
|
||||||
return transaction_id
|
|
||||||
|
|
|
@ -18,12 +18,11 @@ import typing
|
||||||
|
|
||||||
from solana.publickey import PublicKey
|
from solana.publickey import PublicKey
|
||||||
|
|
||||||
|
from .account import Account
|
||||||
from .accountinfo import AccountInfo
|
from .accountinfo import AccountInfo
|
||||||
from .constants import SYSTEM_PROGRAM_ADDRESS
|
from .constants import SYSTEM_PROGRAM_ADDRESS
|
||||||
from .context import Context
|
from .context import Context
|
||||||
from .group import Group
|
from .group import Group
|
||||||
from .marginaccount import MarginAccount
|
|
||||||
from .openorders import OpenOrders
|
|
||||||
from .tokenaccount import TokenAccount
|
from .tokenaccount import TokenAccount
|
||||||
from .wallet import Wallet
|
from .wallet import Wallet
|
||||||
|
|
||||||
|
@ -144,29 +143,19 @@ class AccountScout:
|
||||||
return report
|
return report
|
||||||
|
|
||||||
# Must have token accounts for each of the tokens in the group's basket.
|
# Must have token accounts for each of the tokens in the group's basket.
|
||||||
for basket_token in group.basket_tokens:
|
for basket_token in group.tokens:
|
||||||
token_accounts = TokenAccount.fetch_all_for_owner_and_token(context, account_address, basket_token.token)
|
if basket_token is not None:
|
||||||
if len(token_accounts) == 0:
|
token_accounts = TokenAccount.fetch_all_for_owner_and_token(
|
||||||
report.add_error(
|
context, account_address, basket_token.token)
|
||||||
f"Account '{account_address}' has no account for token '{basket_token.token.name}', mint '{basket_token.token.mint}'.")
|
if len(token_accounts) == 0:
|
||||||
else:
|
report.add_error(
|
||||||
report.add_detail(
|
f"Account '{account_address}' has no account for token '{basket_token.token.name}', mint '{basket_token.token.mint}'.")
|
||||||
f"Account '{account_address}' has {len(token_accounts)} {basket_token.token.name} token account(s) with mint '{basket_token.token.mint}': {[ta.address for ta in token_accounts]}")
|
else:
|
||||||
|
|
||||||
# Should have an open orders account for each market in the group. (Only required for re-balancing via Serum, which isn't implemented here yet.)
|
|
||||||
for market in group.markets:
|
|
||||||
open_orders = OpenOrders.load_for_market_and_owner(
|
|
||||||
context, market.address, account_address, context.dex_program_id, market.base.token.decimals, market.quote.token.decimals)
|
|
||||||
if len(open_orders) == 0:
|
|
||||||
report.add_warning(
|
|
||||||
f"No Serum open orders account for market '{market.base.token.name}/{market.quote.token.name}' [{market.address}]'.")
|
|
||||||
else:
|
|
||||||
for open_orders_account in open_orders:
|
|
||||||
report.add_detail(
|
report.add_detail(
|
||||||
f"Serum open orders account for market '{market.base.token.name}/{market.quote.token.name}': {open_orders_account}")
|
f"Account '{account_address}' has {len(token_accounts)} {basket_token.token.name} token account(s) with mint '{basket_token.token.mint}': {[ta.address for ta in token_accounts]}")
|
||||||
|
|
||||||
# May have one or more Mango Markets margin account, but it's optional for liquidating
|
# May have one or more Mango Markets margin account, but it's optional for liquidating
|
||||||
margin_accounts = MarginAccount.load_all_for_owner(context, account_address, group)
|
margin_accounts = Account.load_all_for_owner(context, account_address, group)
|
||||||
if len(margin_accounts) == 0:
|
if len(margin_accounts) == 0:
|
||||||
report.add_detail(f"Account '{account_address}' has no Mango Markets margin accounts.")
|
report.add_detail(f"Account '{account_address}' has no Mango Markets margin accounts.")
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -24,7 +24,7 @@ from .accountinfo import AccountInfo
|
||||||
|
|
||||||
# # 🥭 AddressableAccount class
|
# # 🥭 AddressableAccount class
|
||||||
#
|
#
|
||||||
# Some of our most-used objects (like `Group` or `MarginAccount`) are accounts on Solana
|
# Some of our most-used objects (like `Group` or `Account`) are accounts on Solana
|
||||||
# with packed data. When these are loaded, they're typically loaded by loading the
|
# with packed data. When these are loaded, they're typically loaded by loading the
|
||||||
# `AccountInfo` and parsing it in an object-specific way.
|
# `AccountInfo` and parsing it in an object-specific way.
|
||||||
#
|
#
|
||||||
|
|
|
@ -1,166 +0,0 @@
|
||||||
# # ⚠ Warning
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
|
||||||
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
|
||||||
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
||||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
#
|
|
||||||
# [🥭 Mango Markets](https://mango.markets/) support is available at:
|
|
||||||
# [Docs](https://docs.mango.markets/)
|
|
||||||
# [Discord](https://discord.gg/67jySBhxrg)
|
|
||||||
# [Twitter](https://twitter.com/mangomarkets)
|
|
||||||
# [Github](https://github.com/blockworks-foundation)
|
|
||||||
# [Email](mailto:hello@blockworks.foundation)
|
|
||||||
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from decimal import Decimal
|
|
||||||
from solana.publickey import PublicKey
|
|
||||||
|
|
||||||
from .accountinfo import AccountInfo
|
|
||||||
from .addressableaccount import AddressableAccount
|
|
||||||
from .context import Context
|
|
||||||
from .layouts import layouts
|
|
||||||
from .version import Version
|
|
||||||
|
|
||||||
|
|
||||||
# # 🥭 AggregatorConfig class
|
|
||||||
#
|
|
||||||
|
|
||||||
class AggregatorConfig:
|
|
||||||
def __init__(self, version: Version, description: str, decimals: Decimal, restart_delay: Decimal,
|
|
||||||
max_submissions: Decimal, min_submissions: Decimal, reward_amount: Decimal,
|
|
||||||
reward_token_account: PublicKey):
|
|
||||||
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
|
||||||
self.version: Version = version
|
|
||||||
self.description: str = description
|
|
||||||
self.decimals: Decimal = decimals
|
|
||||||
self.restart_delay: Decimal = restart_delay
|
|
||||||
self.max_submissions: Decimal = max_submissions
|
|
||||||
self.min_submissions: Decimal = min_submissions
|
|
||||||
self.reward_amount: Decimal = reward_amount
|
|
||||||
self.reward_token_account: PublicKey = reward_token_account
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_layout(layout: layouts.AGGREGATOR_CONFIG) -> "AggregatorConfig":
|
|
||||||
return AggregatorConfig(Version.UNSPECIFIED, layout.description, layout.decimals,
|
|
||||||
layout.restart_delay, layout.max_submissions, layout.min_submissions,
|
|
||||||
layout.reward_amount, layout.reward_token_account)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"« AggregatorConfig: '{self.description}', Decimals: {self.decimals} [restart delay: {self.restart_delay}], Max: {self.max_submissions}, Min: {self.min_submissions}, Reward: {self.reward_amount}, Reward Account: {self.reward_token_account} »"
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"{self}"
|
|
||||||
|
|
||||||
|
|
||||||
# # 🥭 Round class
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
class Round:
|
|
||||||
def __init__(self, version: Version, id: Decimal, created_at: datetime.datetime, updated_at: datetime.datetime):
|
|
||||||
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
|
||||||
self.version: Version = version
|
|
||||||
self.id: Decimal = id
|
|
||||||
self.created_at: datetime.datetime = created_at
|
|
||||||
self.updated_at: datetime.datetime = updated_at
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_layout(layout: layouts.ROUND) -> "Round":
|
|
||||||
return Round(Version.UNSPECIFIED, layout.id, layout.created_at, layout.updated_at)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"« Round[{self.id}], Created: {self.updated_at}, Updated: {self.updated_at} »"
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"{self}"
|
|
||||||
|
|
||||||
|
|
||||||
# # 🥭 Answer class
|
|
||||||
#
|
|
||||||
|
|
||||||
class Answer:
|
|
||||||
def __init__(self, version: Version, round_id: Decimal, median: Decimal, created_at: datetime.datetime, updated_at: datetime.datetime):
|
|
||||||
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
|
||||||
self.version: Version = version
|
|
||||||
self.round_id: Decimal = round_id
|
|
||||||
self.median: Decimal = median
|
|
||||||
self.created_at: datetime.datetime = created_at
|
|
||||||
self.updated_at: datetime.datetime = updated_at
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_layout(layout: layouts.ANSWER) -> "Answer":
|
|
||||||
return Answer(Version.UNSPECIFIED, layout.round_id, layout.median, layout.created_at, layout.updated_at)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"« Answer: Round[{self.round_id}], Median: {self.median:,.8f}, Created: {self.updated_at}, Updated: {self.updated_at} »"
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"{self}"
|
|
||||||
|
|
||||||
# # 🥭 Aggregator class
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
class Aggregator(AddressableAccount):
|
|
||||||
def __init__(self, account_info: AccountInfo, version: Version, config: AggregatorConfig,
|
|
||||||
initialized: bool, name: str, owner: PublicKey, round_: Round,
|
|
||||||
round_submissions: PublicKey, answer: Answer, answer_submissions: PublicKey):
|
|
||||||
super().__init__(account_info)
|
|
||||||
self.version: Version = version
|
|
||||||
self.config: AggregatorConfig = config
|
|
||||||
self.initialized: bool = initialized
|
|
||||||
self.name: str = name
|
|
||||||
self.owner: PublicKey = owner
|
|
||||||
self.round: Round = round_
|
|
||||||
self.round_submissions: PublicKey = round_submissions
|
|
||||||
self.answer: Answer = answer
|
|
||||||
self.answer_submissions: PublicKey = answer_submissions
|
|
||||||
|
|
||||||
@property
|
|
||||||
def price(self) -> Decimal:
|
|
||||||
return self.answer.median / (10 ** self.config.decimals)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_layout(layout: layouts.AGGREGATOR, account_info: AccountInfo, name: str) -> "Aggregator":
|
|
||||||
config = AggregatorConfig.from_layout(layout.config)
|
|
||||||
initialized = bool(layout.initialized)
|
|
||||||
round_ = Round.from_layout(layout.round)
|
|
||||||
answer = Answer.from_layout(layout.answer)
|
|
||||||
|
|
||||||
return Aggregator(account_info, Version.UNSPECIFIED, config, initialized, name, layout.owner,
|
|
||||||
round_, layout.round_submissions, answer, layout.answer_submissions)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse(context: Context, account_info: AccountInfo) -> "Aggregator":
|
|
||||||
data = account_info.data
|
|
||||||
if len(data) != layouts.AGGREGATOR.sizeof():
|
|
||||||
raise Exception(f"Data length ({len(data)}) does not match expected size ({layouts.AGGREGATOR.sizeof()})")
|
|
||||||
|
|
||||||
name = context.lookup_oracle_name(account_info.address)
|
|
||||||
layout = layouts.AGGREGATOR.parse(data)
|
|
||||||
return Aggregator.from_layout(layout, account_info, name)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def load(context: Context, account_address: PublicKey):
|
|
||||||
account_info = AccountInfo.load(context, account_address)
|
|
||||||
if account_info is None:
|
|
||||||
raise Exception(f"Aggregator account not found at address '{account_address}'")
|
|
||||||
return Aggregator.parse(context, account_info)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"""
|
|
||||||
« Aggregator '{self.name}' [{self.version}]:
|
|
||||||
Config: {self.config}
|
|
||||||
Initialized: {self.initialized}
|
|
||||||
Owner: {self.owner}
|
|
||||||
Round: {self.round}
|
|
||||||
Round Submissions: {self.round_submissions}
|
|
||||||
Answer: {self.answer}
|
|
||||||
Answer Submissions: {self.answer_submissions}
|
|
||||||
»
|
|
||||||
"""
|
|
|
@ -1,80 +0,0 @@
|
||||||
# # ⚠ Warning
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
|
||||||
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
|
||||||
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
||||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
#
|
|
||||||
# [🥭 Mango Markets](https://mango.markets/) support is available at:
|
|
||||||
# [Docs](https://docs.mango.markets/)
|
|
||||||
# [Discord](https://discord.gg/67jySBhxrg)
|
|
||||||
# [Twitter](https://twitter.com/mangomarkets)
|
|
||||||
# [Github](https://github.com/blockworks-foundation)
|
|
||||||
# [Email](mailto:hello@blockworks.foundation)
|
|
||||||
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import typing
|
|
||||||
|
|
||||||
from solana.publickey import PublicKey
|
|
||||||
|
|
||||||
from .index import Index
|
|
||||||
from .token import Token
|
|
||||||
|
|
||||||
|
|
||||||
# # 🥭 BasketToken class
|
|
||||||
#
|
|
||||||
# `BasketToken` defines aspects of `Token`s that are part of a `Group` basket.
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
class BasketToken:
|
|
||||||
def __init__(self, token: Token, vault: PublicKey, index: Index):
|
|
||||||
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
|
||||||
self.token: Token = token
|
|
||||||
self.vault: PublicKey = vault
|
|
||||||
self.index: Index = index
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def find_by_symbol(values: typing.List["BasketToken"], symbol: str) -> "BasketToken":
|
|
||||||
found = [value for value in values if value.token.symbol_matches(symbol)]
|
|
||||||
if len(found) == 0:
|
|
||||||
raise Exception(f"Token '{symbol}' not found in token values: {values}")
|
|
||||||
|
|
||||||
if len(found) > 1:
|
|
||||||
raise Exception(f"Token '{symbol}' matched multiple tokens in values: {values}")
|
|
||||||
|
|
||||||
return found[0]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def find_by_mint(values: typing.List["BasketToken"], mint: PublicKey) -> "BasketToken":
|
|
||||||
found = [value for value in values if value.token.mint == mint]
|
|
||||||
if len(found) == 0:
|
|
||||||
raise Exception(f"Token '{mint}' not found in token values: {values}")
|
|
||||||
|
|
||||||
if len(found) > 1:
|
|
||||||
raise Exception(f"Token '{mint}' matched multiple tokens in values: {values}")
|
|
||||||
|
|
||||||
return found[0]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def find_by_token(values: typing.List["BasketToken"], token: Token) -> "BasketToken":
|
|
||||||
return BasketToken.find_by_mint(values, token.mint)
|
|
||||||
|
|
||||||
# BasketTokens are equal if they have the same underlying token.
|
|
||||||
def __eq__(self, other):
|
|
||||||
if hasattr(other, 'token'):
|
|
||||||
return self.token == other.token
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
index = str(self.index).replace("\n", "\n ")
|
|
||||||
return f"""« BasketToken:
|
|
||||||
{self.token}
|
|
||||||
Vault: {self.vault}
|
|
||||||
Index: {index}
|
|
||||||
»"""
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"{self}"
|
|
|
@ -83,7 +83,7 @@ class Context:
|
||||||
self.commitment: Commitment = Commitment("processed")
|
self.commitment: Commitment = Commitment("processed")
|
||||||
self.transaction_options: TxOpts = TxOpts(preflight_commitment=self.commitment)
|
self.transaction_options: TxOpts = TxOpts(preflight_commitment=self.commitment)
|
||||||
self.encoding: str = "base64"
|
self.encoding: str = "base64"
|
||||||
ids_json_token_lookup: TokenLookup = IdsJsonTokenLookup(cluster)
|
ids_json_token_lookup: TokenLookup = IdsJsonTokenLookup(cluster, group_name)
|
||||||
spl_token_lookup: TokenLookup = SplTokenLookup.load(token_filename)
|
spl_token_lookup: TokenLookup = SplTokenLookup.load(token_filename)
|
||||||
all_token_lookup: TokenLookup = CompoundTokenLookup(
|
all_token_lookup: TokenLookup = CompoundTokenLookup(
|
||||||
[ids_json_token_lookup, spl_token_lookup])
|
[ids_json_token_lookup, spl_token_lookup])
|
||||||
|
@ -96,7 +96,7 @@ class Context:
|
||||||
|
|
||||||
# kangda said in Discord: https://discord.com/channels/791995070613159966/836239696467591186/847816026245693451
|
# kangda said in Discord: https://discord.com/channels/791995070613159966/836239696467591186/847816026245693451
|
||||||
# "I think you are better off doing 4,8,16,20,30"
|
# "I think you are better off doing 4,8,16,20,30"
|
||||||
self.retry_pauses: typing.List[Decimal] = [Decimal(4), Decimal(
|
self.retry_pauses: typing.Sequence[Decimal] = [Decimal(4), Decimal(
|
||||||
8), Decimal(16), Decimal(20), Decimal(30)]
|
8), Decimal(16), Decimal(20), Decimal(30)]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -16,11 +16,11 @@
|
||||||
|
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
|
from .account import Account
|
||||||
from .context import Context
|
from .context import Context
|
||||||
from .mangoaccount import MangoAccount
|
from .group import Group
|
||||||
from .market import Market
|
from .market import Market
|
||||||
from .marketoperations import MarketOperations, NullMarketOperations
|
from .marketoperations import MarketOperations, NullMarketOperations
|
||||||
from .mangogroup import MangoGroup
|
|
||||||
from .perpmarket import PerpMarket
|
from .perpmarket import PerpMarket
|
||||||
from .perpmarketoperations import PerpMarketOperations
|
from .perpmarketoperations import PerpMarketOperations
|
||||||
# from .serummarketoperations import SerumMarketOperations
|
# from .serummarketoperations import SerumMarketOperations
|
||||||
|
@ -38,8 +38,8 @@ def create_market_operations(context: Context, wallet: Wallet, dry_run: bool, ma
|
||||||
elif isinstance(market, SpotMarket):
|
elif isinstance(market, SpotMarket):
|
||||||
# return SerumMarketOperations(context, wallet, market, reporter)
|
# return SerumMarketOperations(context, wallet, market, reporter)
|
||||||
# elif isinstance(market, PerpMarket):
|
# elif isinstance(market, PerpMarket):
|
||||||
group = MangoGroup.load(context, context.group_id)
|
group = Group.load(context, context.group_id)
|
||||||
margin_accounts = MangoAccount.load_all_for_owner(context, wallet.address, group)
|
margin_accounts = Account.load_all_for_owner(context, wallet.address, group)
|
||||||
perp_market_info = group.perp_markets[0]
|
perp_market_info = group.perp_markets[0]
|
||||||
if perp_market_info is None:
|
if perp_market_info is None:
|
||||||
raise Exception("Perp market not found at index 0.")
|
raise Exception("Perp market not found at index 0.")
|
||||||
|
|
|
@ -38,7 +38,7 @@ from solana.publickey import PublicKey
|
||||||
# `decode_binary()` decodes the data properly based on which encoding was used.
|
# `decode_binary()` decodes the data properly based on which encoding was used.
|
||||||
|
|
||||||
|
|
||||||
def decode_binary(encoded: typing.List) -> bytes:
|
def decode_binary(encoded: typing.Sequence) -> bytes:
|
||||||
if isinstance(encoded, str):
|
if isinstance(encoded, str):
|
||||||
return base58.b58decode(encoded)
|
return base58.b58decode(encoded)
|
||||||
elif encoded[1] == "base64":
|
elif encoded[1] == "base64":
|
||||||
|
@ -53,7 +53,7 @@ def decode_binary(encoded: typing.List) -> bytes:
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
def encode_binary(decoded: bytes) -> typing.List:
|
def encode_binary(decoded: bytes) -> typing.Sequence:
|
||||||
return [base64.b64encode(decoded), "base64"]
|
return [base64.b64encode(decoded), "base64"]
|
||||||
|
|
||||||
|
|
||||||
|
|
241
mango/group.py
241
mango/group.py
|
@ -13,8 +13,6 @@
|
||||||
# [Github](https://github.com/blockworks-foundation)
|
# [Github](https://github.com/blockworks-foundation)
|
||||||
# [Email](mailto:hello@blockworks.foundation)
|
# [Email](mailto:hello@blockworks.foundation)
|
||||||
|
|
||||||
import construct
|
|
||||||
import time
|
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
@ -22,203 +20,126 @@ from solana.publickey import PublicKey
|
||||||
|
|
||||||
from .accountinfo import AccountInfo
|
from .accountinfo import AccountInfo
|
||||||
from .addressableaccount import AddressableAccount
|
from .addressableaccount import AddressableAccount
|
||||||
from .aggregator import Aggregator
|
|
||||||
from .baskettoken import BasketToken
|
|
||||||
from .context import Context
|
from .context import Context
|
||||||
from .index import Index
|
|
||||||
from .layouts import layouts
|
from .layouts import layouts
|
||||||
from .mangoaccountflags import MangoAccountFlags
|
|
||||||
from .marketmetadata import MarketMetadata
|
|
||||||
from .marketlookup import MarketLookup
|
from .marketlookup import MarketLookup
|
||||||
from .token import SolToken, Token
|
from .metadata import Metadata
|
||||||
|
from .perpmarketinfo import PerpMarketInfo
|
||||||
|
from .spotmarketinfo import SpotMarketInfo
|
||||||
|
from .token import SolToken
|
||||||
|
from .tokeninfo import TokenInfo
|
||||||
from .tokenlookup import TokenLookup
|
from .tokenlookup import TokenLookup
|
||||||
from .tokenvalue import TokenValue
|
from .tokenvalue import TokenValue
|
||||||
from .version import Version
|
from .version import Version
|
||||||
|
|
||||||
|
|
||||||
# # 🥭 Group class
|
# # 🥭 Group class
|
||||||
#
|
#
|
||||||
# The `Group` class encapsulates the data for the Mango Group - the cross-margined basket
|
# `Group` defines root functionality for Mango Markets.
|
||||||
# of tokens with lending.
|
#
|
||||||
|
|
||||||
|
|
||||||
class Group(AddressableAccount):
|
class Group(AddressableAccount):
|
||||||
def __init__(self, account_info: AccountInfo, version: Version, name: str,
|
def __init__(self, account_info: AccountInfo, version: Version, name: str,
|
||||||
account_flags: MangoAccountFlags, basket_tokens: typing.List[BasketToken],
|
meta_data: Metadata, tokens: typing.Sequence[typing.Optional[TokenInfo]],
|
||||||
markets: typing.List[MarketMetadata],
|
spot_markets: typing.Sequence[typing.Optional[SpotMarketInfo]],
|
||||||
signer_nonce: Decimal, signer_key: PublicKey, dex_program_id: PublicKey,
|
perp_markets: typing.Sequence[typing.Optional[PerpMarketInfo]],
|
||||||
total_deposits: typing.List[TokenValue], total_borrows: typing.List[TokenValue],
|
oracles: typing.Sequence[PublicKey], signer_nonce: Decimal, signer_key: PublicKey,
|
||||||
maint_coll_ratio: Decimal, init_coll_ratio: Decimal, srm_vault: PublicKey,
|
admin: PublicKey, dex_program_id: PublicKey, cache: PublicKey, valid_interval: Decimal):
|
||||||
admin: PublicKey, borrow_limits: typing.List[TokenValue]):
|
|
||||||
super().__init__(account_info)
|
super().__init__(account_info)
|
||||||
self.version: Version = version
|
self.version: Version = version
|
||||||
self.name: str = name
|
self.name: str = name
|
||||||
self.account_flags: MangoAccountFlags = account_flags
|
|
||||||
self.basket_tokens: typing.List[BasketToken] = basket_tokens
|
self.meta_data: Metadata = meta_data
|
||||||
self.markets: typing.List[MarketMetadata] = markets
|
self.tokens: typing.Sequence[typing.Optional[TokenInfo]] = tokens
|
||||||
|
self.spot_markets: typing.Sequence[typing.Optional[SpotMarketInfo]] = spot_markets
|
||||||
|
self.perp_markets: typing.Sequence[typing.Optional[PerpMarketInfo]] = perp_markets
|
||||||
|
self.oracles: typing.Sequence[PublicKey] = oracles
|
||||||
self.signer_nonce: Decimal = signer_nonce
|
self.signer_nonce: Decimal = signer_nonce
|
||||||
self.signer_key: PublicKey = signer_key
|
self.signer_key: PublicKey = signer_key
|
||||||
self.dex_program_id: PublicKey = dex_program_id
|
|
||||||
self.total_deposits: typing.List[TokenValue] = total_deposits
|
|
||||||
self.total_borrows: typing.List[TokenValue] = total_borrows
|
|
||||||
self.maint_coll_ratio: Decimal = maint_coll_ratio
|
|
||||||
self.init_coll_ratio: Decimal = init_coll_ratio
|
|
||||||
self.srm_vault: PublicKey = srm_vault
|
|
||||||
self.admin: PublicKey = admin
|
self.admin: PublicKey = admin
|
||||||
self.borrow_limits: typing.List[TokenValue] = borrow_limits
|
self.dex_program_id: PublicKey = dex_program_id
|
||||||
|
self.cache: PublicKey = cache
|
||||||
|
self.valid_interval: Decimal = valid_interval
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def shared_quote_token(self) -> BasketToken:
|
def shared_quote_token(self) -> TokenInfo:
|
||||||
return self.basket_tokens[-1]
|
quote = self.tokens[-1]
|
||||||
|
if quote is None:
|
||||||
|
raise Exception(f"Could not find shared quote token for group '{self.name}'.")
|
||||||
|
return quote
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def base_tokens(self) -> typing.List[BasketToken]:
|
def base_tokens(self) -> typing.Sequence[typing.Optional[TokenInfo]]:
|
||||||
return self.basket_tokens[:-1]
|
return self.tokens[:-1]
|
||||||
|
|
||||||
# When loading from a layout, we ignore mint_decimals. In this Discord from Daffy:
|
|
||||||
# https://discord.com/channels/791995070613159966/818978757648842782/851481660049850388
|
|
||||||
# he says it's:
|
|
||||||
# > same as what is stored on on chain Mint
|
|
||||||
# > Cached on the group so we don't have to pass in the mint every time
|
|
||||||
#
|
|
||||||
# Since we already have that value from our `Token` we don't need to use it and we can
|
|
||||||
# stick with passing around `Token` objects.
|
|
||||||
#
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_layout(layout: construct.Struct, name: str, account_info: AccountInfo, version: Version, token_lookup: TokenLookup, market_lookup: MarketLookup) -> "Group":
|
def from_layout(layout: layouts.GROUP, name: str, account_info: AccountInfo, version: Version, token_lookup: TokenLookup, market_lookup: MarketLookup) -> "Group":
|
||||||
account_flags: MangoAccountFlags = MangoAccountFlags.from_layout(layout.account_flags)
|
meta_data = Metadata.from_layout(layout.meta_data)
|
||||||
|
num_oracles = layout.num_oracles
|
||||||
|
tokens = [TokenInfo.from_layout_or_none(t, token_lookup) for t in layout.tokens]
|
||||||
|
spot_markets = [SpotMarketInfo.from_layout_or_none(m, market_lookup) for m in layout.spot_markets]
|
||||||
|
perp_markets = [PerpMarketInfo.from_layout_or_none(p) for p in layout.perp_markets]
|
||||||
|
oracles = list(layout.oracles)[:int(num_oracles)]
|
||||||
|
signer_nonce = layout.signer_nonce
|
||||||
|
signer_key = layout.signer_key
|
||||||
|
admin = layout.admin
|
||||||
|
dex_program_id = layout.dex_program_id
|
||||||
|
cache = layout.cache
|
||||||
|
valid_interval = layout.valid_interval
|
||||||
|
|
||||||
basket_tokens: typing.List[BasketToken] = []
|
return Group(account_info, version, name, meta_data, tokens, spot_markets, perp_markets, oracles, signer_nonce, signer_key, admin, dex_program_id, cache, valid_interval)
|
||||||
total_deposits: typing.List[TokenValue] = []
|
|
||||||
total_borrows: typing.List[TokenValue] = []
|
|
||||||
borrow_limits: typing.List[TokenValue] = []
|
|
||||||
for index, token_address in enumerate(layout.tokens):
|
|
||||||
static_token_data = token_lookup.find_by_mint(token_address)
|
|
||||||
if static_token_data is None:
|
|
||||||
raise Exception(f"Could not find token with mint '{token_address}'.")
|
|
||||||
|
|
||||||
# We create a new Token object here specifically to force the use of our own decimals
|
|
||||||
token = Token(static_token_data.symbol, static_token_data.name, token_address, layout.mint_decimals[index])
|
|
||||||
token_index = Index.from_layout(layout.indexes[index], token)
|
|
||||||
basket_token = BasketToken(token, layout.vaults[index], token_index)
|
|
||||||
basket_tokens += [basket_token]
|
|
||||||
total_deposits += [TokenValue(token, token.shift_to_decimals(layout.total_deposits[index]))]
|
|
||||||
total_borrows += [TokenValue(token, token.shift_to_decimals(layout.total_borrows[index]))]
|
|
||||||
borrow_limits += [TokenValue(token, token.shift_to_decimals(layout.borrow_limits[index]))]
|
|
||||||
|
|
||||||
markets: typing.List[MarketMetadata] = []
|
|
||||||
for index, market_address in enumerate(layout.spot_markets):
|
|
||||||
spot_market = market_lookup.find_by_address(market_address)
|
|
||||||
if spot_market is None:
|
|
||||||
raise Exception(f"Could not find spot market with address '{market_address}'.")
|
|
||||||
|
|
||||||
base_token = BasketToken.find_by_mint(basket_tokens, spot_market.base.mint)
|
|
||||||
quote_token = BasketToken.find_by_mint(basket_tokens, spot_market.quote.mint)
|
|
||||||
|
|
||||||
market = MarketMetadata(spot_market.symbol, market_address, base_token, quote_token,
|
|
||||||
spot_market, layout.oracles[index], layout.oracle_decimals[index])
|
|
||||||
markets += [market]
|
|
||||||
|
|
||||||
maint_coll_ratio = layout.maint_coll_ratio.quantize(Decimal('.01'))
|
|
||||||
init_coll_ratio = layout.init_coll_ratio.quantize(Decimal('.01'))
|
|
||||||
|
|
||||||
return Group(account_info, version, name, account_flags, basket_tokens, markets,
|
|
||||||
layout.signer_nonce, layout.signer_key, layout.dex_program_id, total_deposits,
|
|
||||||
total_borrows, maint_coll_ratio, init_coll_ratio, layout.srm_vault,
|
|
||||||
layout.admin, borrow_limits)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse(context: Context, account_info: AccountInfo) -> "Group":
|
def parse(context: Context, account_info: AccountInfo) -> "Group":
|
||||||
data = account_info.data
|
data = account_info.data
|
||||||
if len(data) == layouts.GROUP_V1.sizeof():
|
if len(data) != layouts.GROUP.sizeof():
|
||||||
layout = layouts.GROUP_V1.parse(data)
|
|
||||||
version: Version = Version.V1
|
|
||||||
elif len(data) == layouts.GROUP_V2.sizeof():
|
|
||||||
version = Version.V2
|
|
||||||
layout = layouts.GROUP_V2.parse(data)
|
|
||||||
else:
|
|
||||||
raise Exception(
|
raise Exception(
|
||||||
f"Group data length ({len(data)}) does not match expected size ({layouts.GROUP_V1.sizeof()} or {layouts.GROUP_V2.sizeof()})")
|
f"Group data length ({len(data)}) does not match expected size ({layouts.GROUP.sizeof()}")
|
||||||
|
|
||||||
return Group.from_layout(layout, context.group_name, account_info, version, context.token_lookup, context.market_lookup)
|
layout = layouts.GROUP.parse(data)
|
||||||
|
return Group.from_layout(layout, "merps_test_v3", account_info, Version.V1, context.token_lookup, context.market_lookup)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load(context: Context):
|
def load(context: Context, address: typing.Optional[PublicKey] = None) -> "Group":
|
||||||
account_info = AccountInfo.load(context, context.group_id)
|
group_address: PublicKey = address or context.group_id
|
||||||
|
account_info = AccountInfo.load(context, group_address)
|
||||||
if account_info is None:
|
if account_info is None:
|
||||||
raise Exception(f"Group account not found at address '{context.group_id}'")
|
raise Exception(f"Group account not found at address '{group_address}'")
|
||||||
return Group.parse(context, account_info)
|
return Group.parse(context, account_info)
|
||||||
|
|
||||||
def price_index_of_token(self, token: Token) -> int:
|
def fetch_balances(self, context: Context, root_address: PublicKey) -> typing.Sequence[TokenValue]:
|
||||||
for index, existing in enumerate(self.basket_tokens):
|
|
||||||
if existing.token == token:
|
|
||||||
return index
|
|
||||||
return -1
|
|
||||||
|
|
||||||
def fetch_token_prices(self, context: Context) -> typing.List[TokenValue]:
|
|
||||||
started_at = time.time()
|
|
||||||
|
|
||||||
# Note: we can just load the oracle data in a simpler way, with:
|
|
||||||
# oracles = map(lambda market: Aggregator.load(context, market.oracle), self.markets)
|
|
||||||
# but that makes a network request for every oracle. We can reduce that to just one request
|
|
||||||
# if we use AccountInfo.load_multiple() and parse the data ourselves.
|
|
||||||
#
|
|
||||||
# This seems to halve the time this function takes.
|
|
||||||
oracle_addresses = list([market.oracle for market in self.markets])
|
|
||||||
oracle_account_infos = AccountInfo.load_multiple(context, oracle_addresses)
|
|
||||||
oracles = map(lambda oracle_account_info: Aggregator.parse(context, oracle_account_info), oracle_account_infos)
|
|
||||||
prices = list(map(lambda oracle: oracle.price, oracles)) + [Decimal(1)]
|
|
||||||
token_prices = []
|
|
||||||
for index, price in enumerate(prices):
|
|
||||||
token_prices += [TokenValue(self.basket_tokens[index].token, price)]
|
|
||||||
|
|
||||||
time_taken = time.time() - started_at
|
|
||||||
self.logger.info(f"Fetching prices complete. Time taken: {time_taken:.2f} seconds.")
|
|
||||||
return token_prices
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def load_with_prices(context: Context) -> typing.Tuple["Group", typing.List[TokenValue]]:
|
|
||||||
group = Group.load(context)
|
|
||||||
prices = group.fetch_token_prices(context)
|
|
||||||
return group, prices
|
|
||||||
|
|
||||||
def fetch_balances(self, context: Context, root_address: PublicKey) -> typing.List[TokenValue]:
|
|
||||||
balances: typing.List[TokenValue] = []
|
balances: typing.List[TokenValue] = []
|
||||||
sol_balance = context.fetch_sol_balance(root_address)
|
sol_balance = context.fetch_sol_balance(root_address)
|
||||||
balances += [TokenValue(SolToken, sol_balance)]
|
balances += [TokenValue(SolToken, sol_balance)]
|
||||||
|
|
||||||
for basket_token in self.basket_tokens:
|
for basket_token in self.tokens:
|
||||||
balance = TokenValue.fetch_total_value(context, root_address, basket_token.token)
|
if basket_token is not None and basket_token.token is not None:
|
||||||
balances += [balance]
|
balance = TokenValue.fetch_total_value(context, root_address, basket_token.token)
|
||||||
|
balances += [balance]
|
||||||
return balances
|
return balances
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self):
|
||||||
total_deposits = "\n ".join(map(str, self.total_deposits))
|
tokens = "\n ".join([f"{token}".replace("\n", "\n ")
|
||||||
total_borrows = "\n ".join(map(str, self.total_borrows))
|
for token in self.tokens if token is not None])
|
||||||
borrow_limits = "\n ".join(map(str, self.borrow_limits))
|
spot_markets = "\n ".join([f"{spot_market}".replace("\n", "\n ")
|
||||||
shared_quote_token = str(self.shared_quote_token).replace("\n", "\n ")
|
for spot_market in self.spot_markets if spot_market is not None])
|
||||||
base_tokens = "\n ".join([f"{tok}".replace("\n", "\n ") for tok in self.base_tokens])
|
perp_markets = "\n ".join([f"{perp_market}".replace("\n", "\n ")
|
||||||
markets = "\n ".join([f"{mkt}".replace("\n", "\n ") for mkt in self.markets])
|
for perp_market in self.perp_markets if perp_market is not None])
|
||||||
return f"""
|
oracles = "\n ".join([f"{oracle}" for oracle in self.oracles])
|
||||||
« Group [{self.version} - {self.name}] {self.address}:
|
return f"""« 𝙶𝚛𝚘𝚞𝚙 {self.version} [{self.address}]
|
||||||
Flags: {self.account_flags}
|
{self.meta_data}
|
||||||
Base Tokens:
|
Name: {self.name}
|
||||||
{base_tokens}
|
Signer [Nonce: {self.signer_nonce}]: {self.signer_key}
|
||||||
Quote Token:
|
Admin: {self.admin}
|
||||||
{shared_quote_token}
|
DEX Program ID: {self.dex_program_id}
|
||||||
Markets:
|
Merps Cache: {self.cache}
|
||||||
{markets}
|
Valid Interval: {self.valid_interval}
|
||||||
DEX Program ID: « {self.dex_program_id} »
|
Tokens:
|
||||||
SRM Vault: « {self.srm_vault} »
|
{tokens}
|
||||||
Admin: « {self.admin} »
|
Spot Markets:
|
||||||
Signer Nonce: {self.signer_nonce}
|
{spot_markets}
|
||||||
Signer Key: « {self.signer_key} »
|
Perp Markets:
|
||||||
Initial Collateral Ratio: {self.init_coll_ratio}
|
{perp_markets}
|
||||||
Maintenance Collateral Ratio: {self.maint_coll_ratio}
|
Oracles:
|
||||||
Total Deposits:
|
{oracles}
|
||||||
{total_deposits}
|
»"""
|
||||||
Total Borrows:
|
|
||||||
{total_borrows}
|
|
||||||
Borrow Limits:
|
|
||||||
{borrow_limits}
|
|
||||||
»
|
|
||||||
"""
|
|
||||||
|
|
|
@ -29,13 +29,14 @@ from .tokenlookup import TokenLookup
|
||||||
#
|
#
|
||||||
|
|
||||||
class IdsJsonTokenLookup(TokenLookup):
|
class IdsJsonTokenLookup(TokenLookup):
|
||||||
def __init__(self, cluster: str) -> None:
|
def __init__(self, cluster: str, group_name: str) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.cluster: str = cluster
|
self.cluster: str = cluster
|
||||||
|
self.group_name: str = group_name
|
||||||
|
|
||||||
def find_by_symbol(self, symbol: str) -> typing.Optional[Token]:
|
def find_by_symbol(self, symbol: str) -> typing.Optional[Token]:
|
||||||
for group in MangoConstants["groups"]:
|
for group in MangoConstants["groups"]:
|
||||||
if group["cluster"] == self.cluster:
|
if group["cluster"] == self.cluster and group["name"] == self.group_name:
|
||||||
for token in group["tokens"]:
|
for token in group["tokens"]:
|
||||||
if token["symbol"] == symbol:
|
if token["symbol"] == symbol:
|
||||||
return Token(token["symbol"], token["symbol"], PublicKey(token["mintKey"]), Decimal(token["decimals"]))
|
return Token(token["symbol"], token["symbol"], PublicKey(token["mintKey"]), Decimal(token["decimals"]))
|
||||||
|
@ -44,7 +45,7 @@ class IdsJsonTokenLookup(TokenLookup):
|
||||||
def find_by_mint(self, mint: PublicKey) -> typing.Optional[Token]:
|
def find_by_mint(self, mint: PublicKey) -> typing.Optional[Token]:
|
||||||
mint_str = str(mint)
|
mint_str = str(mint)
|
||||||
for group in MangoConstants["groups"]:
|
for group in MangoConstants["groups"]:
|
||||||
if group["cluster"] == self.cluster:
|
if group["cluster"] == self.cluster and group["name"] == self.group_name:
|
||||||
for token in group["tokens"]:
|
for token in group["tokens"]:
|
||||||
if token["mintKey"] == mint_str:
|
if token["mintKey"] == mint_str:
|
||||||
return Token(token["symbol"], token["symbol"], PublicKey(token["mintKey"]), Decimal(token["decimals"]))
|
return Token(token["symbol"], token["symbol"], PublicKey(token["mintKey"]), Decimal(token["decimals"]))
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
# # ⚠ Warning
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
|
||||||
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
|
||||||
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
||||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
#
|
|
||||||
# [🥭 Mango Markets](https://mango.markets/) support is available at:
|
|
||||||
# [Docs](https://docs.mango.markets/)
|
|
||||||
# [Discord](https://discord.gg/67jySBhxrg)
|
|
||||||
# [Twitter](https://twitter.com/mangomarkets)
|
|
||||||
# [Github](https://github.com/blockworks-foundation)
|
|
||||||
# [Email](mailto:hello@blockworks.foundation)
|
|
||||||
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
from .layouts import layouts
|
|
||||||
from .token import Token
|
|
||||||
from .tokenvalue import TokenValue
|
|
||||||
from .version import Version
|
|
||||||
|
|
||||||
|
|
||||||
# # 🥭 Index class
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
class Index:
|
|
||||||
def __init__(self, version: Version, token: Token, last_update: datetime.datetime, borrow: TokenValue, deposit: TokenValue):
|
|
||||||
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
|
||||||
self.version: Version = version
|
|
||||||
self.token: Token = token
|
|
||||||
self.last_update: datetime.datetime = last_update
|
|
||||||
self.borrow: TokenValue = borrow
|
|
||||||
self.deposit: TokenValue = deposit
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_layout(layout: layouts.INDEX, token: Token) -> "Index":
|
|
||||||
borrow = TokenValue(token, layout.borrow / Decimal(10 ** token.decimals))
|
|
||||||
deposit = TokenValue(token, layout.deposit / Decimal(10 ** token.decimals))
|
|
||||||
return Index(Version.UNSPECIFIED, token, layout.last_update, borrow, deposit)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"""« Index [{self.token.symbol}] ({self.last_update}):
|
|
||||||
Borrow: {self.borrow},
|
|
||||||
Deposit: {self.deposit} »"""
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"{self}"
|
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
import logging
|
import logging
|
||||||
import struct
|
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
@ -28,20 +27,12 @@ from solana.account import Account
|
||||||
from solana.publickey import PublicKey
|
from solana.publickey import PublicKey
|
||||||
from solana.system_program import CreateAccountParams, create_account
|
from solana.system_program import CreateAccountParams, create_account
|
||||||
from solana.transaction import AccountMeta, TransactionInstruction
|
from solana.transaction import AccountMeta, TransactionInstruction
|
||||||
from solana.sysvar import SYSVAR_CLOCK_PUBKEY
|
|
||||||
from spl.token.constants import ACCOUNT_LEN, TOKEN_PROGRAM_ID
|
from spl.token.constants import ACCOUNT_LEN, TOKEN_PROGRAM_ID
|
||||||
from spl.token.instructions import CloseAccountParams, InitializeAccountParams, Transfer2Params, close_account, initialize_account, transfer2
|
from spl.token.instructions import CloseAccountParams, InitializeAccountParams, Transfer2Params, close_account, initialize_account, transfer2
|
||||||
|
|
||||||
from .baskettoken import BasketToken
|
|
||||||
from .constants import SYSTEM_PROGRAM_ADDRESS
|
|
||||||
from .context import Context
|
from .context import Context
|
||||||
from .group import Group
|
|
||||||
from .layouts import layouts
|
from .layouts import layouts
|
||||||
from .marginaccount import MarginAccount
|
|
||||||
from .marketmetadata import MarketMetadata
|
|
||||||
from .token import Token
|
from .token import Token
|
||||||
from .tokenaccount import TokenAccount
|
|
||||||
from .tokenvalue import TokenValue
|
|
||||||
from .wallet import Wallet
|
from .wallet import Wallet
|
||||||
|
|
||||||
|
|
||||||
|
@ -69,394 +60,6 @@ class InstructionBuilder(metaclass=abc.ABCMeta):
|
||||||
return f"{self}"
|
return f"{self}"
|
||||||
|
|
||||||
|
|
||||||
# # 🥭 ForceCancelOrdersInstructionBuilder class
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# ## Rust Interface
|
|
||||||
#
|
|
||||||
# This is what the `force_cancel_orders` instruction looks like in the [Mango Rust](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs) code:
|
|
||||||
# ```
|
|
||||||
# pub fn force_cancel_orders(
|
|
||||||
# program_id: &Pubkey,
|
|
||||||
# mango_group_pk: &Pubkey,
|
|
||||||
# liqor_pk: &Pubkey,
|
|
||||||
# liqee_margin_account_acc: &Pubkey,
|
|
||||||
# base_vault_pk: &Pubkey,
|
|
||||||
# quote_vault_pk: &Pubkey,
|
|
||||||
# spot_market_pk: &Pubkey,
|
|
||||||
# bids_pk: &Pubkey,
|
|
||||||
# asks_pk: &Pubkey,
|
|
||||||
# signer_pk: &Pubkey,
|
|
||||||
# dex_event_queue_pk: &Pubkey,
|
|
||||||
# dex_base_pk: &Pubkey,
|
|
||||||
# dex_quote_pk: &Pubkey,
|
|
||||||
# dex_signer_pk: &Pubkey,
|
|
||||||
# dex_prog_id: &Pubkey,
|
|
||||||
# open_orders_pks: &[Pubkey],
|
|
||||||
# oracle_pks: &[Pubkey],
|
|
||||||
# limit: u8
|
|
||||||
# ) -> Result<Instruction, ProgramError>
|
|
||||||
# ```
|
|
||||||
#
|
|
||||||
# ## Client API call
|
|
||||||
#
|
|
||||||
# This is how it is built using the Mango Markets client API:
|
|
||||||
# ```
|
|
||||||
# const keys = [
|
|
||||||
# { isSigner: false, isWritable: true, pubkey: mangoGroup },
|
|
||||||
# { isSigner: true, isWritable: false, pubkey: liqor },
|
|
||||||
# { isSigner: false, isWritable: true, pubkey: liqeeMarginAccount },
|
|
||||||
# { isSigner: false, isWritable: true, pubkey: baseVault },
|
|
||||||
# { isSigner: false, isWritable: true, pubkey: quoteVault },
|
|
||||||
# { isSigner: false, isWritable: true, pubkey: spotMarket },
|
|
||||||
# { isSigner: false, isWritable: true, pubkey: bids },
|
|
||||||
# { isSigner: false, isWritable: true, pubkey: asks },
|
|
||||||
# { isSigner: false, isWritable: false, pubkey: signerKey },
|
|
||||||
# { isSigner: false, isWritable: true, pubkey: dexEventQueue },
|
|
||||||
# { isSigner: false, isWritable: true, pubkey: dexBaseVault },
|
|
||||||
# { isSigner: false, isWritable: true, pubkey: dexQuoteVault },
|
|
||||||
# { isSigner: false, isWritable: false, pubkey: dexSigner },
|
|
||||||
# { isSigner: false, isWritable: false, pubkey: TOKEN_PROGRAM_ID },
|
|
||||||
# { isSigner: false, isWritable: false, pubkey: dexProgramId },
|
|
||||||
# { isSigner: false, isWritable: false, pubkey: SYSVAR_CLOCK_PUBKEY },
|
|
||||||
# ...openOrders.map((pubkey) => ({
|
|
||||||
# isSigner: false,
|
|
||||||
# isWritable: true,
|
|
||||||
# pubkey,
|
|
||||||
# })),
|
|
||||||
# ...oracles.map((pubkey) => ({
|
|
||||||
# isSigner: false,
|
|
||||||
# isWritable: false,
|
|
||||||
# pubkey,
|
|
||||||
# })),
|
|
||||||
# ];
|
|
||||||
#
|
|
||||||
# const data = encodeMangoInstruction({ ForceCancelOrders: { limit } });
|
|
||||||
# return new TransactionInstruction({ keys, data, programId });
|
|
||||||
# ```
|
|
||||||
#
|
|
||||||
|
|
||||||
class ForceCancelOrdersInstructionBuilder(InstructionBuilder):
|
|
||||||
# We can create up to a maximum of max_instructions instructions. I'm not sure of the reason
|
|
||||||
# for this threshold but it's what's in the original liquidator source code and I'm assuming
|
|
||||||
# it's there for a good reason.
|
|
||||||
max_instructions: int = 10
|
|
||||||
|
|
||||||
# We cancel up to max_cancels_per_instruction orders with each instruction.
|
|
||||||
max_cancels_per_instruction: int = 5
|
|
||||||
|
|
||||||
def __init__(self, context: Context, group: Group, wallet: Wallet, margin_account: MarginAccount, market_metadata: MarketMetadata, market: Market, oracles: typing.List[PublicKey], dex_signer: PublicKey):
|
|
||||||
super().__init__(context)
|
|
||||||
self.group = group
|
|
||||||
self.wallet = wallet
|
|
||||||
self.margin_account = margin_account
|
|
||||||
self.market_metadata = market_metadata
|
|
||||||
self.market = market
|
|
||||||
self.oracles = oracles
|
|
||||||
self.dex_signer = dex_signer
|
|
||||||
|
|
||||||
def build(self) -> TransactionInstruction:
|
|
||||||
transaction = TransactionInstruction(
|
|
||||||
keys=[
|
|
||||||
AccountMeta(is_signer=False, is_writable=True, pubkey=self.group.address),
|
|
||||||
AccountMeta(is_signer=True, is_writable=False, pubkey=self.wallet.address),
|
|
||||||
AccountMeta(is_signer=False, is_writable=True, pubkey=self.margin_account.address),
|
|
||||||
AccountMeta(is_signer=False, is_writable=True, pubkey=self.market_metadata.base.vault),
|
|
||||||
AccountMeta(is_signer=False, is_writable=True, pubkey=self.market_metadata.quote.vault),
|
|
||||||
AccountMeta(is_signer=False, is_writable=True, pubkey=self.market_metadata.spot.address),
|
|
||||||
AccountMeta(is_signer=False, is_writable=True, pubkey=self.market.state.bids()),
|
|
||||||
AccountMeta(is_signer=False, is_writable=True, pubkey=self.market.state.asks()),
|
|
||||||
AccountMeta(is_signer=False, is_writable=False, pubkey=self.group.signer_key),
|
|
||||||
AccountMeta(is_signer=False, is_writable=True, pubkey=self.market.state.event_queue()),
|
|
||||||
AccountMeta(is_signer=False, is_writable=True, pubkey=self.market.state.base_vault()),
|
|
||||||
AccountMeta(is_signer=False, is_writable=True, pubkey=self.market.state.quote_vault()),
|
|
||||||
AccountMeta(is_signer=False, is_writable=False, pubkey=self.dex_signer),
|
|
||||||
AccountMeta(is_signer=False, is_writable=False, pubkey=TOKEN_PROGRAM_ID),
|
|
||||||
AccountMeta(is_signer=False, is_writable=False, pubkey=self.context.dex_program_id),
|
|
||||||
AccountMeta(is_signer=False, is_writable=False, pubkey=SYSVAR_CLOCK_PUBKEY),
|
|
||||||
*list([AccountMeta(is_signer=False, is_writable=oo_address is None, pubkey=oo_address or SYSTEM_PROGRAM_ADDRESS)
|
|
||||||
for oo_address in self.margin_account.open_orders]),
|
|
||||||
*list([AccountMeta(is_signer=False, is_writable=False, pubkey=oracle_address) for oracle_address in self.oracles])
|
|
||||||
],
|
|
||||||
program_id=self.context.program_id,
|
|
||||||
data=layouts.FORCE_CANCEL_ORDERS.build(
|
|
||||||
{"limit": ForceCancelOrdersInstructionBuilder.max_cancels_per_instruction})
|
|
||||||
)
|
|
||||||
self.logger.debug(f"Built transaction: {transaction}")
|
|
||||||
return transaction
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_margin_account_and_market(context: Context, group: Group, wallet: Wallet, margin_account: MarginAccount, market_metadata: MarketMetadata) -> "ForceCancelOrdersInstructionBuilder":
|
|
||||||
market = market_metadata.fetch_market(context)
|
|
||||||
nonce = struct.pack("<Q", market.state.vault_signer_nonce())
|
|
||||||
dex_signer = PublicKey.create_program_address(
|
|
||||||
[bytes(market_metadata.spot.address), nonce], context.dex_program_id)
|
|
||||||
oracles = list([mkt.oracle for mkt in group.markets])
|
|
||||||
|
|
||||||
return ForceCancelOrdersInstructionBuilder(context, group, wallet, margin_account, market_metadata, market, oracles, dex_signer)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def multiple_instructions_from_margin_account_and_market(cls, context: Context, group: Group, wallet: Wallet, margin_account: MarginAccount, market_metadata: MarketMetadata, at_least_this_many_cancellations: int) -> typing.List["ForceCancelOrdersInstructionBuilder"]:
|
|
||||||
logger: logging.Logger = logging.getLogger(cls.__name__)
|
|
||||||
|
|
||||||
# We cancel up to max_cancels_per_instruction orders with each instruction, but if
|
|
||||||
# we have more than cancel_limit we create more instructions (each handling up to
|
|
||||||
# 5 orders)
|
|
||||||
calculated_instruction_count = int(
|
|
||||||
at_least_this_many_cancellations / ForceCancelOrdersInstructionBuilder.max_cancels_per_instruction) + 1
|
|
||||||
|
|
||||||
# We create a maximum of max_instructions instructions.
|
|
||||||
instruction_count = min(calculated_instruction_count, ForceCancelOrdersInstructionBuilder.max_instructions)
|
|
||||||
|
|
||||||
instructions: typing.List[ForceCancelOrdersInstructionBuilder] = []
|
|
||||||
for counter in range(instruction_count):
|
|
||||||
instructions += [ForceCancelOrdersInstructionBuilder.from_margin_account_and_market(
|
|
||||||
context, group, wallet, margin_account, market_metadata)]
|
|
||||||
|
|
||||||
logger.debug(f"Built {len(instructions)} transaction(s).")
|
|
||||||
|
|
||||||
return instructions
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
# Print the members out using the Rust parameter order and names.
|
|
||||||
return f"""« ForceCancelOrdersInstructionBuilder:
|
|
||||||
program_id: &Pubkey: {self.context.program_id},
|
|
||||||
mango_group_pk: &Pubkey: {self.group.address},
|
|
||||||
liqor_pk: &Pubkey: {self.wallet.address},
|
|
||||||
liqee_margin_account_acc: &Pubkey: {self.margin_account.address},
|
|
||||||
base_vault_pk: &Pubkey: {self.market_metadata.base.vault},
|
|
||||||
quote_vault_pk: &Pubkey: {self.market_metadata.quote.vault},
|
|
||||||
spot_market_pk: &Pubkey: {self.market_metadata.spot.address},
|
|
||||||
bids_pk: &Pubkey: {self.market.state.bids()},
|
|
||||||
asks_pk: &Pubkey: {self.market.state.asks()},
|
|
||||||
signer_pk: &Pubkey: {self.group.signer_key},
|
|
||||||
dex_event_queue_pk: &Pubkey: {self.market.state.event_queue()},
|
|
||||||
dex_base_pk: &Pubkey: {self.market.state.base_vault()},
|
|
||||||
dex_quote_pk: &Pubkey: {self.market.state.quote_vault()},
|
|
||||||
dex_signer_pk: &Pubkey: {self.dex_signer},
|
|
||||||
dex_prog_id: &Pubkey: {self.context.dex_program_id},
|
|
||||||
open_orders_pks: &[Pubkey]: {self.margin_account.open_orders},
|
|
||||||
oracle_pks: &[Pubkey]: {self.oracles},
|
|
||||||
limit: u8: {ForceCancelOrdersInstructionBuilder.max_cancels_per_instruction}
|
|
||||||
»"""
|
|
||||||
|
|
||||||
|
|
||||||
# # 🥭 LiquidateInstructionBuilder class
|
|
||||||
#
|
|
||||||
# This is the `Instruction` we send to Solana to perform the (partial) liquidation.
|
|
||||||
#
|
|
||||||
# We take care to pass the proper high-level parameters to the `LiquidateInstructionBuilder`
|
|
||||||
# constructor so that `build_transaction()` is straightforward. That tends to push
|
|
||||||
# complexities to `from_margin_account_and_market()` though.
|
|
||||||
#
|
|
||||||
# ## Rust Interface
|
|
||||||
#
|
|
||||||
# This is what the `partial_liquidate` instruction looks like in the [Mango Rust](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs) code:
|
|
||||||
# ```
|
|
||||||
# /// Take over a MarginAccount that is below init_coll_ratio by depositing funds
|
|
||||||
# ///
|
|
||||||
# /// Accounts expected by this instruction (10 + 2 * NUM_MARKETS):
|
|
||||||
# ///
|
|
||||||
# /// 0. `[writable]` mango_group_acc - MangoGroup that this margin account is for
|
|
||||||
# /// 1. `[signer]` liqor_acc - liquidator's solana account
|
|
||||||
# /// 2. `[writable]` liqor_in_token_acc - liquidator's token account to deposit
|
|
||||||
# /// 3. `[writable]` liqor_out_token_acc - liquidator's token account to withdraw into
|
|
||||||
# /// 4. `[writable]` liqee_margin_account_acc - MarginAccount of liquidatee
|
|
||||||
# /// 5. `[writable]` in_vault_acc - Mango vault of in_token
|
|
||||||
# /// 6. `[writable]` out_vault_acc - Mango vault of out_token
|
|
||||||
# /// 7. `[]` signer_acc
|
|
||||||
# /// 8. `[]` token_prog_acc - Token program id
|
|
||||||
# /// 9. `[]` clock_acc - Clock sysvar account
|
|
||||||
# /// 10..10+NUM_MARKETS `[]` open_orders_accs - open orders for each of the spot market
|
|
||||||
# /// 10+NUM_MARKETS..10+2*NUM_MARKETS `[]`
|
|
||||||
# /// oracle_accs - flux aggregator feed accounts
|
|
||||||
# ```
|
|
||||||
#
|
|
||||||
# ```
|
|
||||||
# pub fn partial_liquidate(
|
|
||||||
# program_id: &Pubkey,
|
|
||||||
# mango_group_pk: &Pubkey,
|
|
||||||
# liqor_pk: &Pubkey,
|
|
||||||
# liqor_in_token_pk: &Pubkey,
|
|
||||||
# liqor_out_token_pk: &Pubkey,
|
|
||||||
# liqee_margin_account_acc: &Pubkey,
|
|
||||||
# in_vault_pk: &Pubkey,
|
|
||||||
# out_vault_pk: &Pubkey,
|
|
||||||
# signer_pk: &Pubkey,
|
|
||||||
# open_orders_pks: &[Pubkey],
|
|
||||||
# oracle_pks: &[Pubkey],
|
|
||||||
# max_deposit: u64
|
|
||||||
# ) -> Result<Instruction, ProgramError>
|
|
||||||
# ```
|
|
||||||
#
|
|
||||||
# ## Client API call
|
|
||||||
#
|
|
||||||
# This is how it is built using the Mango Markets client API:
|
|
||||||
# ```
|
|
||||||
# const keys = [
|
|
||||||
# { isSigner: false, isWritable: true, pubkey: mangoGroup },
|
|
||||||
# { isSigner: true, isWritable: false, pubkey: liqor },
|
|
||||||
# { isSigner: false, isWritable: true, pubkey: liqorInTokenWallet },
|
|
||||||
# { isSigner: false, isWritable: true, pubkey: liqorOutTokenWallet },
|
|
||||||
# { isSigner: false, isWritable: true, pubkey: liqeeMarginAccount },
|
|
||||||
# { isSigner: false, isWritable: true, pubkey: inTokenVault },
|
|
||||||
# { isSigner: false, isWritable: true, pubkey: outTokenVault },
|
|
||||||
# { isSigner: false, isWritable: false, pubkey: signerKey },
|
|
||||||
# { isSigner: false, isWritable: false, pubkey: TOKEN_PROGRAM_ID },
|
|
||||||
# { isSigner: false, isWritable: false, pubkey: SYSVAR_CLOCK_PUBKEY },
|
|
||||||
# ...openOrders.map((pubkey) => ({
|
|
||||||
# isSigner: false,
|
|
||||||
# isWritable: false,
|
|
||||||
# pubkey,
|
|
||||||
# })),
|
|
||||||
# ...oracles.map((pubkey) => ({
|
|
||||||
# isSigner: false,
|
|
||||||
# isWritable: false,
|
|
||||||
# pubkey,
|
|
||||||
# })),
|
|
||||||
# ];
|
|
||||||
# const data = encodeMangoInstruction({ PartialLiquidate: { maxDeposit } });
|
|
||||||
#
|
|
||||||
# return new TransactionInstruction({ keys, data, programId });
|
|
||||||
# ```
|
|
||||||
#
|
|
||||||
# ## from_margin_account_and_market() function
|
|
||||||
#
|
|
||||||
# `from_margin_account_and_market()` merits a bit of explaining.
|
|
||||||
#
|
|
||||||
# `from_margin_account_and_market()` takes (among other things) a `Wallet` and a
|
|
||||||
# `MarginAccount`. The idea is that the `MarginAccount` has some assets in one token, and
|
|
||||||
# some liabilities in some different token.
|
|
||||||
#
|
|
||||||
# To liquidate the account, we want to:
|
|
||||||
# * supply tokens from the `Wallet` in the token currency that has the greatest liability
|
|
||||||
# value in the `MarginAccount`
|
|
||||||
# * receive tokens in the `Wallet` in the token currency that has the greatest asset value
|
|
||||||
# in the `MarginAccount`
|
|
||||||
#
|
|
||||||
# So we calculate the token currencies from the largest liabilities and assets in the
|
|
||||||
# `MarginAccount`, but we use those token types to get the correct `Wallet` accounts.
|
|
||||||
# * `input_token` is the `BasketToken` of the currency the `Wallet` is _paying_ and the
|
|
||||||
# `MarginAccount` is _receiving_ to pay off its largest liability.
|
|
||||||
# * `output_token` is the `BasketToken` of the currency the `Wallet` is _receiving_ and the
|
|
||||||
# `MarginAccount` is _paying_ from its largest asset.
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
class LiquidateInstructionBuilder(InstructionBuilder):
|
|
||||||
def __init__(self, context: Context, group: Group, wallet: Wallet, margin_account: MarginAccount, oracles: typing.List[PublicKey], input_token: BasketToken, output_token: BasketToken, wallet_input_token_account: TokenAccount, wallet_output_token_account: TokenAccount, maximum_input_amount: Decimal):
|
|
||||||
super().__init__(context)
|
|
||||||
self.group: Group = group
|
|
||||||
self.wallet: Wallet = wallet
|
|
||||||
self.margin_account: MarginAccount = margin_account
|
|
||||||
self.oracles: typing.List[PublicKey] = oracles
|
|
||||||
self.input_token: BasketToken = input_token
|
|
||||||
self.output_token: BasketToken = output_token
|
|
||||||
self.wallet_input_token_account: TokenAccount = wallet_input_token_account
|
|
||||||
self.wallet_output_token_account: TokenAccount = wallet_output_token_account
|
|
||||||
self.maximum_input_amount: Decimal = maximum_input_amount
|
|
||||||
|
|
||||||
def build(self) -> TransactionInstruction:
|
|
||||||
transaction = TransactionInstruction(
|
|
||||||
keys=[
|
|
||||||
AccountMeta(is_signer=False, is_writable=True, pubkey=self.group.address),
|
|
||||||
AccountMeta(is_signer=True, is_writable=False, pubkey=self.wallet.address),
|
|
||||||
AccountMeta(is_signer=False, is_writable=True, pubkey=self.wallet_input_token_account.address),
|
|
||||||
AccountMeta(is_signer=False, is_writable=True, pubkey=self.wallet_output_token_account.address),
|
|
||||||
AccountMeta(is_signer=False, is_writable=True, pubkey=self.margin_account.address),
|
|
||||||
AccountMeta(is_signer=False, is_writable=True, pubkey=self.input_token.vault),
|
|
||||||
AccountMeta(is_signer=False, is_writable=True, pubkey=self.output_token.vault),
|
|
||||||
AccountMeta(is_signer=False, is_writable=False, pubkey=self.group.signer_key),
|
|
||||||
AccountMeta(is_signer=False, is_writable=False, pubkey=TOKEN_PROGRAM_ID),
|
|
||||||
AccountMeta(is_signer=False, is_writable=False, pubkey=SYSVAR_CLOCK_PUBKEY),
|
|
||||||
*list([AccountMeta(is_signer=False, is_writable=oo_address is None, pubkey=oo_address or SYSTEM_PROGRAM_ADDRESS)
|
|
||||||
for oo_address in self.margin_account.open_orders]),
|
|
||||||
*list([AccountMeta(is_signer=False, is_writable=False, pubkey=oracle_address) for oracle_address in self.oracles])
|
|
||||||
],
|
|
||||||
program_id=self.context.program_id,
|
|
||||||
data=layouts.PARTIAL_LIQUIDATE.build({"max_deposit": int(self.maximum_input_amount)})
|
|
||||||
)
|
|
||||||
self.logger.debug(f"Built transaction: {transaction}")
|
|
||||||
return transaction
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_margin_account_and_market(cls, context: Context, group: Group, wallet: Wallet, margin_account: MarginAccount, prices: typing.List[TokenValue]) -> typing.Optional["LiquidateInstructionBuilder"]:
|
|
||||||
logger: logging.Logger = logging.getLogger(cls.__name__)
|
|
||||||
|
|
||||||
oracles = list([mkt.oracle for mkt in group.markets])
|
|
||||||
|
|
||||||
balance_sheets = margin_account.get_priced_balance_sheets(group, prices)
|
|
||||||
|
|
||||||
sorted_by_assets = sorted(balance_sheets, key=lambda sheet: sheet.assets, reverse=True)
|
|
||||||
sorted_by_liabilities = sorted(balance_sheets, key=lambda sheet: sheet.liabilities, reverse=True)
|
|
||||||
|
|
||||||
most_assets = sorted_by_assets[0]
|
|
||||||
most_liabilities = sorted_by_liabilities[0]
|
|
||||||
if most_assets.token == most_liabilities.token:
|
|
||||||
# If there's a weirdness where the account with the biggest assets is also the one
|
|
||||||
# with the biggest liabilities, pick the next-best one by assets.
|
|
||||||
logger.info(
|
|
||||||
f"Switching asset token from {most_assets.token.name} to {sorted_by_assets[1].token.name} because {most_liabilities.token.name} is the token with most liabilities.")
|
|
||||||
most_assets = sorted_by_assets[1]
|
|
||||||
|
|
||||||
logger.info(f"Most assets: {most_assets}")
|
|
||||||
logger.info(f"Most liabilities: {most_liabilities}")
|
|
||||||
|
|
||||||
most_assets_basket_token = BasketToken.find_by_token(group.basket_tokens, most_assets.token)
|
|
||||||
most_liabilities_basket_token = BasketToken.find_by_token(group.basket_tokens, most_liabilities.token)
|
|
||||||
logger.info(f"Most assets basket token: {most_assets_basket_token}")
|
|
||||||
logger.info(f"Most liabilities basket token: {most_liabilities_basket_token}")
|
|
||||||
|
|
||||||
if most_assets.value == Decimal(0):
|
|
||||||
logger.warning(f"Margin account {margin_account.address} has no assets to take.")
|
|
||||||
return None
|
|
||||||
|
|
||||||
if most_liabilities.value == Decimal(0):
|
|
||||||
logger.warning(f"Margin account {margin_account.address} has no liabilities to fund.")
|
|
||||||
return None
|
|
||||||
|
|
||||||
wallet_input_token_account = TokenAccount.fetch_largest_for_owner_and_token(
|
|
||||||
context, wallet.address, most_liabilities.token)
|
|
||||||
if wallet_input_token_account is None:
|
|
||||||
raise Exception(f"Could not load wallet input token account for mint '{most_liabilities.token.mint}'")
|
|
||||||
|
|
||||||
if wallet_input_token_account.value.value == Decimal(0):
|
|
||||||
logger.warning(
|
|
||||||
f"Wallet token account {wallet_input_token_account.address} has no tokens to send that could fund a liquidation.")
|
|
||||||
return None
|
|
||||||
|
|
||||||
wallet_output_token_account = TokenAccount.fetch_largest_for_owner_and_token(
|
|
||||||
context, wallet.address, most_assets.token)
|
|
||||||
if wallet_output_token_account is None:
|
|
||||||
raise Exception(f"Could not load wallet output token account for mint '{most_assets.token.mint}'")
|
|
||||||
|
|
||||||
# Convert the token amount to the native representation
|
|
||||||
maximum_input_value = wallet_input_token_account.value
|
|
||||||
maximum_input_amount = maximum_input_value.token.shift_to_native(maximum_input_value.value)
|
|
||||||
return LiquidateInstructionBuilder(context, group, wallet, margin_account, oracles,
|
|
||||||
most_liabilities_basket_token, most_assets_basket_token,
|
|
||||||
wallet_input_token_account,
|
|
||||||
wallet_output_token_account,
|
|
||||||
maximum_input_amount)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
# Print the members out using the Rust parameter order and names.
|
|
||||||
return f"""« LiquidateInstructionBuilder:
|
|
||||||
program_id: &Pubkey: {self.context.program_id},
|
|
||||||
mango_group_pk: &Pubkey: {self.group.address},
|
|
||||||
liqor_pk: &Pubkey: {self.wallet.address},
|
|
||||||
liqor_in_token_pk: &Pubkey: {self.wallet_input_token_account.address},
|
|
||||||
liqor_out_token_pk: &Pubkey: {self.wallet_output_token_account.address},
|
|
||||||
liqee_margin_account_acc: &Pubkey: {self.margin_account.address},
|
|
||||||
in_vault_pk: &Pubkey: {self.input_token.vault},
|
|
||||||
out_vault_pk: &Pubkey: {self.output_token.vault},
|
|
||||||
signer_pk: &Pubkey: {self.group.signer_key},
|
|
||||||
open_orders_pks: &[Pubkey]: {self.margin_account.open_orders},
|
|
||||||
oracle_pks: &[Pubkey]: {self.oracles},
|
|
||||||
max_deposit: u64: : {self.maximum_input_amount}
|
|
||||||
»"""
|
|
||||||
|
|
||||||
|
|
||||||
# # 🥭 CreateSplAccountInstructionBuilder class
|
# # 🥭 CreateSplAccountInstructionBuilder class
|
||||||
#
|
#
|
||||||
# Creates an SPL token account. Can't do much with it without following by an
|
# Creates an SPL token account. Can't do much with it without following by an
|
||||||
|
@ -616,11 +219,11 @@ class NewOrderV3InstructionBuilder(InstructionBuilder):
|
||||||
# Creates an event-consuming 'crank' instruction.
|
# Creates an event-consuming 'crank' instruction.
|
||||||
#
|
#
|
||||||
class ConsumeEventsInstructionBuilder(InstructionBuilder):
|
class ConsumeEventsInstructionBuilder(InstructionBuilder):
|
||||||
def __init__(self, context: Context, wallet: Wallet, market: Market, open_orders_addresses: typing.List[PublicKey], limit: int = 32):
|
def __init__(self, context: Context, wallet: Wallet, market: Market, open_orders_addresses: typing.Sequence[PublicKey], limit: int = 32):
|
||||||
super().__init__(context)
|
super().__init__(context)
|
||||||
self.wallet: Wallet = wallet
|
self.wallet: Wallet = wallet
|
||||||
self.market: Market = market
|
self.market: Market = market
|
||||||
self.open_orders_addresses: typing.List[PublicKey] = open_orders_addresses
|
self.open_orders_addresses: typing.Sequence[PublicKey] = open_orders_addresses
|
||||||
self.limit: int = limit
|
self.limit: int = limit
|
||||||
|
|
||||||
def build(self) -> TransactionInstruction:
|
def build(self) -> TransactionInstruction:
|
||||||
|
|
|
@ -43,4 +43,4 @@ class InstructionType(enum.IntEnum):
|
||||||
PartialLiquidate = 16
|
PartialLiquidate = 16
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.value
|
||||||
|
|
|
@ -32,7 +32,6 @@
|
||||||
|
|
||||||
import construct
|
import construct
|
||||||
import datetime
|
import datetime
|
||||||
import itertools
|
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
@ -249,9 +248,7 @@ class OrderBookNodeAdapter(construct.Adapter):
|
||||||
|
|
||||||
# # Layout Structs
|
# # Layout Structs
|
||||||
|
|
||||||
# ## SERUM_ACCOUNT_FLAGS
|
# ## ACCOUNT_FLAGS
|
||||||
#
|
|
||||||
# The SERUM_ prefix is because there's also `MANGO_ACCOUNT_FLAGS`.
|
|
||||||
#
|
#
|
||||||
# Here's the [Serum Rust structure](https://github.com/project-serum/serum-dex/blob/master/dex/src/state.rs):
|
# Here's the [Serum Rust structure](https://github.com/project-serum/serum-dex/blob/master/dex/src/state.rs):
|
||||||
# ```
|
# ```
|
||||||
|
@ -270,7 +267,7 @@ class OrderBookNodeAdapter(construct.Adapter):
|
||||||
# ```
|
# ```
|
||||||
|
|
||||||
|
|
||||||
SERUM_ACCOUNT_FLAGS = construct.BitsSwapped(
|
ACCOUNT_FLAGS = construct.BitsSwapped(
|
||||||
construct.BitStruct(
|
construct.BitStruct(
|
||||||
"initialized" / construct.Flag,
|
"initialized" / construct.Flag,
|
||||||
"market" / construct.Flag,
|
"market" / construct.Flag,
|
||||||
|
@ -285,316 +282,6 @@ SERUM_ACCOUNT_FLAGS = construct.BitsSwapped(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ## MANGO_ACCOUNT_FLAGS
|
|
||||||
#
|
|
||||||
# The MANGO_ prefix is because there's also `SERUM_ACCOUNT_FLAGS`.
|
|
||||||
#
|
|
||||||
# The MANGO_ACCOUNT_FLAGS should be exactly 8 bytes.
|
|
||||||
#
|
|
||||||
# Here's the [Mango Rust structure](https://github.com/blockworks-foundation/mango/blob/master/program/src/state.rs):
|
|
||||||
# ```
|
|
||||||
# #[derive(Copy, Clone, BitFlags, Debug, Eq, PartialEq)]
|
|
||||||
# #[repr(u64)]
|
|
||||||
# pub enum AccountFlag {
|
|
||||||
# Initialized = 1u64 << 0,
|
|
||||||
# MangoGroup = 1u64 << 1,
|
|
||||||
# MarginAccount = 1u64 << 2,
|
|
||||||
# MangoSrmAccount = 1u64 << 3
|
|
||||||
# }
|
|
||||||
# ```
|
|
||||||
|
|
||||||
|
|
||||||
MANGO_ACCOUNT_FLAGS = construct.BitsSwapped(
|
|
||||||
construct.BitStruct(
|
|
||||||
"initialized" / construct.Flag,
|
|
||||||
"group" / construct.Flag,
|
|
||||||
"margin_account" / construct.Flag,
|
|
||||||
"srm_account" / construct.Flag,
|
|
||||||
construct.Padding(4 + (7 * 8))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ## INDEX
|
|
||||||
#
|
|
||||||
# Here's the [Mango Rust structure](https://github.com/blockworks-foundation/mango/blob/master/program/src/state.rs):
|
|
||||||
# ```
|
|
||||||
# #[derive(Copy, Clone)]
|
|
||||||
# #[repr(C)]
|
|
||||||
# pub struct MangoIndex {
|
|
||||||
# pub last_update: u64,
|
|
||||||
# pub borrow: U64F64,
|
|
||||||
# pub deposit: U64F64
|
|
||||||
# }
|
|
||||||
# ```
|
|
||||||
|
|
||||||
|
|
||||||
INDEX = construct.Struct(
|
|
||||||
"last_update" / DatetimeAdapter(),
|
|
||||||
"borrow" / FloatAdapter(),
|
|
||||||
"deposit" / FloatAdapter()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ## AGGREGATOR_CONFIG
|
|
||||||
#
|
|
||||||
# Here's the [Flux Rust structure](https://github.com/blockworks-foundation/solana-flux-aggregator/blob/master/program/src/state.rs):
|
|
||||||
# ```
|
|
||||||
# #[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema, Default, PartialEq)]
|
|
||||||
# pub struct AggregatorConfig {
|
|
||||||
# /// description
|
|
||||||
# pub description: [u8; 32],
|
|
||||||
#
|
|
||||||
# /// decimals for this feed
|
|
||||||
# pub decimals: u8,
|
|
||||||
#
|
|
||||||
# /// oracle cannot start a new round until after `restart_relay` rounds
|
|
||||||
# pub restart_delay: u8,
|
|
||||||
#
|
|
||||||
# /// max number of submissions in a round
|
|
||||||
# pub max_submissions: u8,
|
|
||||||
#
|
|
||||||
# /// min number of submissions in a round to resolve an answer
|
|
||||||
# pub min_submissions: u8,
|
|
||||||
#
|
|
||||||
# /// amount of tokens oracles are reward per submission
|
|
||||||
# pub reward_amount: u64,
|
|
||||||
#
|
|
||||||
# /// SPL token account from which to withdraw rewards
|
|
||||||
# pub reward_token_account: PublicKey,
|
|
||||||
# }
|
|
||||||
# ```
|
|
||||||
|
|
||||||
|
|
||||||
AGGREGATOR_CONFIG = construct.Struct(
|
|
||||||
"description" / construct.PaddedString(32, "utf8"),
|
|
||||||
"decimals" / DecimalAdapter(1),
|
|
||||||
"restart_delay" / DecimalAdapter(1),
|
|
||||||
"max_submissions" / DecimalAdapter(1),
|
|
||||||
"min_submissions" / DecimalAdapter(1),
|
|
||||||
"reward_amount" / DecimalAdapter(),
|
|
||||||
"reward_token_account" / PublicKeyAdapter()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ## ROUND
|
|
||||||
#
|
|
||||||
# Here's the [Flux Rust structure](https://github.com/blockworks-foundation/solana-flux-aggregator/blob/master/program/src/state.rs):
|
|
||||||
# ```
|
|
||||||
# #[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema, Default, PartialEq)]
|
|
||||||
# pub struct Round {
|
|
||||||
# pub id: u64,
|
|
||||||
# pub created_at: u64,
|
|
||||||
# pub updated_at: u64,
|
|
||||||
# }
|
|
||||||
# ```
|
|
||||||
|
|
||||||
|
|
||||||
ROUND = construct.Struct(
|
|
||||||
"id" / DecimalAdapter(),
|
|
||||||
"created_at" / DecimalAdapter(),
|
|
||||||
"updated_at" / DecimalAdapter()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ## ANSWER
|
|
||||||
#
|
|
||||||
# Here's the [Flux Rust structure](https://github.com/blockworks-foundation/solana-flux-aggregator/blob/master/program/src/state.rs):
|
|
||||||
# ```
|
|
||||||
# #[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema, Default, PartialEq)]
|
|
||||||
# pub struct Answer {
|
|
||||||
# pub round_id: u64,
|
|
||||||
# pub median: u64,
|
|
||||||
# pub created_at: u64,
|
|
||||||
# pub updated_at: u64,
|
|
||||||
# }
|
|
||||||
# ```
|
|
||||||
|
|
||||||
|
|
||||||
ANSWER = construct.Struct(
|
|
||||||
"round_id" / DecimalAdapter(),
|
|
||||||
"median" / DecimalAdapter(),
|
|
||||||
"created_at" / DatetimeAdapter(),
|
|
||||||
"updated_at" / DatetimeAdapter()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ## AGGREGATOR
|
|
||||||
#
|
|
||||||
# Here's the [Flux Rust structure](https://github.com/blockworks-foundation/solana-flux-aggregator/blob/master/program/src/state.rs):
|
|
||||||
# ```
|
|
||||||
# #[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema, Default, PartialEq)]
|
|
||||||
# pub struct Aggregator {
|
|
||||||
# pub config: AggregatorConfig,
|
|
||||||
# /// is initialized
|
|
||||||
# pub is_initialized: bool,
|
|
||||||
# /// authority
|
|
||||||
# pub owner: PublicKey,
|
|
||||||
# /// current round accepting oracle submissions
|
|
||||||
# pub round: Round,
|
|
||||||
# pub round_submissions: PublicKey, // has_one: Submissions
|
|
||||||
# /// the latest answer resolved
|
|
||||||
# pub answer: Answer,
|
|
||||||
# pub answer_submissions: PublicKey, // has_one: Submissions
|
|
||||||
# }
|
|
||||||
# ```
|
|
||||||
|
|
||||||
|
|
||||||
AGGREGATOR = construct.Struct(
|
|
||||||
"config" / AGGREGATOR_CONFIG,
|
|
||||||
"initialized" / DecimalAdapter(1),
|
|
||||||
"owner" / PublicKeyAdapter(),
|
|
||||||
"round" / ROUND,
|
|
||||||
"round_submissions" / PublicKeyAdapter(),
|
|
||||||
"answer" / ANSWER,
|
|
||||||
"answer_submissions" / PublicKeyAdapter()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ## GROUP_V1
|
|
||||||
#
|
|
||||||
# Groups have a common quote currency, and it's always the last token in the tokens.
|
|
||||||
#
|
|
||||||
# That means the number of markets is number_of_tokens - 1.
|
|
||||||
#
|
|
||||||
# Here's the [Mango Rust structure](https://github.com/blockworks-foundation/mango/blob/master/program/src/state.rs):
|
|
||||||
# ```
|
|
||||||
# #[derive(Copy, Clone)]
|
|
||||||
# #[repr(C)]
|
|
||||||
# pub struct MangoGroup {
|
|
||||||
# pub account_flags: u64,
|
|
||||||
# pub tokens: [Pubkey; NUM_TOKENS], // Last token is shared quote currency
|
|
||||||
# pub vaults: [Pubkey; NUM_TOKENS], // where funds are stored
|
|
||||||
# pub indexes: [MangoIndex; NUM_TOKENS], // to keep track of interest
|
|
||||||
# pub spot_markets: [Pubkey; NUM_MARKETS], // pubkeys to MarketState of serum dex
|
|
||||||
# pub oracles: [Pubkey; NUM_MARKETS], // oracles that give price of each base currency in quote currency
|
|
||||||
# pub signer_nonce: u64,
|
|
||||||
# pub signer_key: Pubkey,
|
|
||||||
# pub dex_program_id: Pubkey, // serum dex program id
|
|
||||||
#
|
|
||||||
# // denominated in Mango index adjusted terms
|
|
||||||
# pub total_deposits: [U64F64; NUM_TOKENS],
|
|
||||||
# pub total_borrows: [U64F64; NUM_TOKENS],
|
|
||||||
#
|
|
||||||
# pub maint_coll_ratio: U64F64, // 1.10
|
|
||||||
# pub init_coll_ratio: U64F64, // 1.20
|
|
||||||
#
|
|
||||||
# pub srm_vault: Pubkey, // holds users SRM for fee reduction
|
|
||||||
#
|
|
||||||
# /// This admin key is only for alpha release and the only power it has is to amend borrow limits
|
|
||||||
# /// If users borrow too much too quickly before liquidators are able to handle the volume,
|
|
||||||
# /// lender funds will be at risk. Hence these borrow limits will be raised slowly
|
|
||||||
# pub admin: Pubkey,
|
|
||||||
# pub borrow_limits: [u64; NUM_TOKENS],
|
|
||||||
#
|
|
||||||
# pub mint_decimals: [u8; NUM_TOKENS],
|
|
||||||
# pub oracle_decimals: [u8; NUM_MARKETS],
|
|
||||||
# pub padding: [u8; MANGO_GROUP_PADDING]
|
|
||||||
# }
|
|
||||||
# impl_loadable!(MangoGroup);
|
|
||||||
# ```
|
|
||||||
|
|
||||||
|
|
||||||
GROUP_V1_NUM_TOKENS = 3
|
|
||||||
GROUP_V1_NUM_MARKETS = GROUP_V1_NUM_TOKENS - 1
|
|
||||||
GROUP_V1_PADDING = 8 - (GROUP_V1_NUM_TOKENS + GROUP_V1_NUM_MARKETS) % 8
|
|
||||||
GROUP_V1 = construct.Struct(
|
|
||||||
"account_flags" / MANGO_ACCOUNT_FLAGS,
|
|
||||||
"tokens" / construct.Array(GROUP_V1_NUM_TOKENS, PublicKeyAdapter()),
|
|
||||||
"vaults" / construct.Array(GROUP_V1_NUM_TOKENS, PublicKeyAdapter()),
|
|
||||||
"indexes" / construct.Array(GROUP_V1_NUM_TOKENS, INDEX),
|
|
||||||
"spot_markets" / construct.Array(GROUP_V1_NUM_MARKETS, PublicKeyAdapter()),
|
|
||||||
"oracles" / construct.Array(GROUP_V1_NUM_MARKETS, PublicKeyAdapter()),
|
|
||||||
"signer_nonce" / DecimalAdapter(),
|
|
||||||
"signer_key" / PublicKeyAdapter(),
|
|
||||||
"dex_program_id" / PublicKeyAdapter(),
|
|
||||||
"total_deposits" / construct.Array(GROUP_V1_NUM_TOKENS, FloatAdapter()),
|
|
||||||
"total_borrows" / construct.Array(GROUP_V1_NUM_TOKENS, FloatAdapter()),
|
|
||||||
"maint_coll_ratio" / FloatAdapter(),
|
|
||||||
"init_coll_ratio" / FloatAdapter(),
|
|
||||||
"srm_vault" / PublicKeyAdapter(),
|
|
||||||
"admin" / PublicKeyAdapter(),
|
|
||||||
"borrow_limits" / construct.Array(GROUP_V1_NUM_TOKENS, DecimalAdapter()),
|
|
||||||
"mint_decimals" / construct.Array(GROUP_V1_NUM_TOKENS, DecimalAdapter(1)),
|
|
||||||
"oracle_decimals" / construct.Array(GROUP_V1_NUM_MARKETS, DecimalAdapter(1)),
|
|
||||||
"padding" / construct.Array(GROUP_V1_PADDING, construct.Padding(1))
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ## GROUP_V2
|
|
||||||
#
|
|
||||||
# Groups have a common quote currency, and it's always the last token in the tokens.
|
|
||||||
#
|
|
||||||
# That means the number of markets is number_of_tokens - 1.
|
|
||||||
#
|
|
||||||
# There is no difference between the V1 and V2 structures except for the number of tokens.
|
|
||||||
# We handle things this way to be consistent with how we handle V1 and V2 `MarginAccount`s.
|
|
||||||
#
|
|
||||||
# Here's the [Mango Rust structure](https://github.com/blockworks-foundation/mango/blob/master/program/src/state.rs):
|
|
||||||
# ```
|
|
||||||
# #[derive(Copy, Clone)]
|
|
||||||
# #[repr(C)]
|
|
||||||
# pub struct MangoGroup {
|
|
||||||
# pub account_flags: u64,
|
|
||||||
# pub tokens: [Pubkey; NUM_TOKENS], // Last token is shared quote currency
|
|
||||||
# pub vaults: [Pubkey; NUM_TOKENS], // where funds are stored
|
|
||||||
# pub indexes: [MangoIndex; NUM_TOKENS], // to keep track of interest
|
|
||||||
# pub spot_markets: [Pubkey; NUM_MARKETS], // pubkeys to MarketState of serum dex
|
|
||||||
# pub oracles: [Pubkey; NUM_MARKETS], // oracles that give price of each base currency in quote currency
|
|
||||||
# pub signer_nonce: u64,
|
|
||||||
# pub signer_key: Pubkey,
|
|
||||||
# pub dex_program_id: Pubkey, // serum dex program id
|
|
||||||
#
|
|
||||||
# // denominated in Mango index adjusted terms
|
|
||||||
# pub total_deposits: [U64F64; NUM_TOKENS],
|
|
||||||
# pub total_borrows: [U64F64; NUM_TOKENS],
|
|
||||||
#
|
|
||||||
# pub maint_coll_ratio: U64F64, // 1.10
|
|
||||||
# pub init_coll_ratio: U64F64, // 1.20
|
|
||||||
#
|
|
||||||
# pub srm_vault: Pubkey, // holds users SRM for fee reduction
|
|
||||||
#
|
|
||||||
# /// This admin key is only for alpha release and the only power it has is to amend borrow limits
|
|
||||||
# /// If users borrow too much too quickly before liquidators are able to handle the volume,
|
|
||||||
# /// lender funds will be at risk. Hence these borrow limits will be raised slowly
|
|
||||||
# /// UPDATE: 4/15/2021 - this admin key is now useless, borrow limits are removed
|
|
||||||
# pub admin: Pubkey,
|
|
||||||
# pub borrow_limits: [u64; NUM_TOKENS],
|
|
||||||
#
|
|
||||||
# pub mint_decimals: [u8; NUM_TOKENS],
|
|
||||||
# pub oracle_decimals: [u8; NUM_MARKETS],
|
|
||||||
# pub padding: [u8; MANGO_GROUP_PADDING]
|
|
||||||
# }
|
|
||||||
#
|
|
||||||
# ```
|
|
||||||
|
|
||||||
|
|
||||||
GROUP_V2_NUM_TOKENS = 5
|
|
||||||
GROUP_V2_NUM_MARKETS = GROUP_V2_NUM_TOKENS - 1
|
|
||||||
GROUP_V2_PADDING = 8 - (GROUP_V2_NUM_TOKENS + GROUP_V2_NUM_MARKETS) % 8
|
|
||||||
GROUP_V2 = construct.Struct(
|
|
||||||
"account_flags" / MANGO_ACCOUNT_FLAGS,
|
|
||||||
"tokens" / construct.Array(GROUP_V2_NUM_TOKENS, PublicKeyAdapter()),
|
|
||||||
"vaults" / construct.Array(GROUP_V2_NUM_TOKENS, PublicKeyAdapter()),
|
|
||||||
"indexes" / construct.Array(GROUP_V2_NUM_TOKENS, INDEX),
|
|
||||||
"spot_markets" / construct.Array(GROUP_V2_NUM_MARKETS, PublicKeyAdapter()),
|
|
||||||
"oracles" / construct.Array(GROUP_V2_NUM_MARKETS, PublicKeyAdapter()),
|
|
||||||
"signer_nonce" / DecimalAdapter(),
|
|
||||||
"signer_key" / PublicKeyAdapter(),
|
|
||||||
"dex_program_id" / PublicKeyAdapter(),
|
|
||||||
"total_deposits" / construct.Array(GROUP_V2_NUM_TOKENS, FloatAdapter()),
|
|
||||||
"total_borrows" / construct.Array(GROUP_V2_NUM_TOKENS, FloatAdapter()),
|
|
||||||
"maint_coll_ratio" / FloatAdapter(),
|
|
||||||
"init_coll_ratio" / FloatAdapter(),
|
|
||||||
"srm_vault" / PublicKeyAdapter(),
|
|
||||||
"admin" / PublicKeyAdapter(),
|
|
||||||
"borrow_limits" / construct.Array(GROUP_V2_NUM_TOKENS, DecimalAdapter()),
|
|
||||||
"mint_decimals" / construct.Array(GROUP_V2_NUM_TOKENS, DecimalAdapter(1)),
|
|
||||||
"oracle_decimals" / construct.Array(GROUP_V2_NUM_MARKETS, DecimalAdapter(1)),
|
|
||||||
"padding" / construct.Array(GROUP_V2_PADDING, construct.Padding(1))
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ## TOKEN_ACCOUNT
|
# ## TOKEN_ACCOUNT
|
||||||
|
|
||||||
|
|
||||||
|
@ -615,7 +302,7 @@ TOKEN_ACCOUNT = construct.Struct(
|
||||||
|
|
||||||
OPEN_ORDERS = construct.Struct(
|
OPEN_ORDERS = construct.Struct(
|
||||||
construct.Padding(5),
|
construct.Padding(5),
|
||||||
"account_flags" / SERUM_ACCOUNT_FLAGS,
|
"account_flags" / ACCOUNT_FLAGS,
|
||||||
"market" / PublicKeyAdapter(),
|
"market" / PublicKeyAdapter(),
|
||||||
"owner" / PublicKeyAdapter(),
|
"owner" / PublicKeyAdapter(),
|
||||||
"base_token_free" / DecimalAdapter(),
|
"base_token_free" / DecimalAdapter(),
|
||||||
|
@ -631,496 +318,6 @@ OPEN_ORDERS = construct.Struct(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ## MARGIN_ACCOUNT_V1
|
|
||||||
#
|
|
||||||
# Here's the V1 [Mango Rust structure](https://github.com/blockworks-foundation/mango/blob/master/program/src/state.rs):
|
|
||||||
# ```
|
|
||||||
# #[derive(Copy, Clone)]
|
|
||||||
# #[repr(C)]
|
|
||||||
# pub struct MarginAccount {
|
|
||||||
# pub account_flags: u64,
|
|
||||||
# pub mango_group: Pubkey,
|
|
||||||
# pub owner: Pubkey, // solana pubkey of owner
|
|
||||||
#
|
|
||||||
# // assets and borrows are denominated in Mango adjusted terms
|
|
||||||
# pub deposits: [U64F64; NUM_TOKENS], // assets being lent out and gaining interest, including collateral
|
|
||||||
#
|
|
||||||
# // this will be incremented every time an order is opened and decremented when order is closed
|
|
||||||
# pub borrows: [U64F64; NUM_TOKENS], // multiply by current index to get actual value
|
|
||||||
#
|
|
||||||
# pub open_orders: [Pubkey; NUM_MARKETS], // owned by Mango
|
|
||||||
#
|
|
||||||
# pub being_liquidated: bool,
|
|
||||||
# pub padding: [u8; 7] // padding to make compatible with previous MarginAccount size
|
|
||||||
# // TODO add has_borrows field for easy memcmp fetching
|
|
||||||
# }
|
|
||||||
# ```
|
|
||||||
|
|
||||||
|
|
||||||
MARGIN_ACCOUNT_V1_NUM_TOKENS = 3
|
|
||||||
MARGIN_ACCOUNT_V1_NUM_MARKETS = MARGIN_ACCOUNT_V1_NUM_TOKENS - 1
|
|
||||||
MARGIN_ACCOUNT_V1 = construct.Struct(
|
|
||||||
"account_flags" / MANGO_ACCOUNT_FLAGS,
|
|
||||||
"mango_group" / PublicKeyAdapter(),
|
|
||||||
"owner" / PublicKeyAdapter(),
|
|
||||||
"deposits" / construct.Array(MARGIN_ACCOUNT_V1_NUM_TOKENS, FloatAdapter()),
|
|
||||||
"borrows" / construct.Array(MARGIN_ACCOUNT_V1_NUM_TOKENS, FloatAdapter()),
|
|
||||||
"open_orders" / construct.Array(MARGIN_ACCOUNT_V1_NUM_MARKETS, PublicKeyAdapter()),
|
|
||||||
"being_liquidated" / DecimalAdapter(1),
|
|
||||||
"padding" / construct.Padding(7)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ## MARGIN_ACCOUNT_V2
|
|
||||||
#
|
|
||||||
# Here's the V2 [Mango Rust structure](https://github.com/blockworks-foundation/mango/blob/master/program/src/state.rs):
|
|
||||||
# ```
|
|
||||||
# #[derive(Copy, Clone)]
|
|
||||||
# #[repr(C)]
|
|
||||||
# pub struct MarginAccount {
|
|
||||||
# pub account_flags: u64,
|
|
||||||
# pub mango_group: Pubkey,
|
|
||||||
# pub owner: Pubkey, // solana pubkey of owner
|
|
||||||
#
|
|
||||||
# // assets and borrows are denominated in Mango adjusted terms
|
|
||||||
# pub deposits: [U64F64; NUM_TOKENS], // assets being lent out and gaining interest, including collateral
|
|
||||||
#
|
|
||||||
# // this will be incremented every time an order is opened and decremented when order is closed
|
|
||||||
# pub borrows: [U64F64; NUM_TOKENS], // multiply by current index to get actual value
|
|
||||||
#
|
|
||||||
# pub open_orders: [Pubkey; NUM_MARKETS], // owned by Mango
|
|
||||||
#
|
|
||||||
# pub being_liquidated: bool,
|
|
||||||
# pub has_borrows: bool, // does the account have any open borrows? set by checked_add_borrow and checked_sub_borrow
|
|
||||||
# pub padding: [u8; 64] // padding
|
|
||||||
# }
|
|
||||||
# ```
|
|
||||||
|
|
||||||
|
|
||||||
MARGIN_ACCOUNT_V2_NUM_TOKENS = 5
|
|
||||||
MARGIN_ACCOUNT_V2_NUM_MARKETS = MARGIN_ACCOUNT_V2_NUM_TOKENS - 1
|
|
||||||
MARGIN_ACCOUNT_V2 = construct.Struct(
|
|
||||||
"account_flags" / MANGO_ACCOUNT_FLAGS,
|
|
||||||
"mango_group" / PublicKeyAdapter(),
|
|
||||||
"owner" / PublicKeyAdapter(),
|
|
||||||
"deposits" / construct.Array(MARGIN_ACCOUNT_V2_NUM_TOKENS, FloatAdapter()),
|
|
||||||
"borrows" / construct.Array(MARGIN_ACCOUNT_V2_NUM_TOKENS, FloatAdapter()),
|
|
||||||
"open_orders" / construct.Array(MARGIN_ACCOUNT_V2_NUM_MARKETS, PublicKeyAdapter()),
|
|
||||||
"being_liquidated" / DecimalAdapter(1),
|
|
||||||
"has_borrows" / DecimalAdapter(1),
|
|
||||||
"padding" / construct.Padding(70)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ## build_margin_account_parser_for_num_tokens() function
|
|
||||||
#
|
|
||||||
# This function builds a `construct.Struct` that can load a `MarginAccount` with a
|
|
||||||
# specific number of tokens. The number of markets and size of padding are derived
|
|
||||||
# from the number of tokens.
|
|
||||||
|
|
||||||
|
|
||||||
def build_margin_account_parser_for_num_tokens(num_tokens: int) -> construct.Struct:
|
|
||||||
num_markets = num_tokens - 1
|
|
||||||
|
|
||||||
return construct.Struct(
|
|
||||||
"account_flags" / MANGO_ACCOUNT_FLAGS,
|
|
||||||
"mango_group" / PublicKeyAdapter(),
|
|
||||||
"owner" / PublicKeyAdapter(),
|
|
||||||
"deposits" / construct.Array(num_tokens, FloatAdapter()),
|
|
||||||
"borrows" / construct.Array(num_tokens, FloatAdapter()),
|
|
||||||
"open_orders" / construct.Array(num_markets, PublicKeyAdapter()),
|
|
||||||
"padding" / construct.Padding(8)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ## build_margin_account_parser_for_length() function
|
|
||||||
#
|
|
||||||
# This function takes a data length (presumably the size of the structure returned from
|
|
||||||
# the `AccountInfo`) and returns a `MarginAccount` structure that can parse it.
|
|
||||||
#
|
|
||||||
# If the size doesn't _exactly_ match the size of the `Struct`, and Exception is raised.
|
|
||||||
|
|
||||||
|
|
||||||
def build_margin_account_parser_for_length(length: int) -> construct.Struct:
|
|
||||||
tried_sizes: typing.List[int] = []
|
|
||||||
for num_tokens in itertools.count(start=2):
|
|
||||||
parser = build_margin_account_parser_for_num_tokens(num_tokens)
|
|
||||||
if parser.sizeof() == length:
|
|
||||||
return parser
|
|
||||||
|
|
||||||
tried_sizes += [parser.sizeof()]
|
|
||||||
if parser.sizeof() > length:
|
|
||||||
raise Exception(
|
|
||||||
f"Could not create MarginAccount parser for length ({length}) - tried sizes ({tried_sizes})")
|
|
||||||
|
|
||||||
|
|
||||||
# # Instruction Structs
|
|
||||||
|
|
||||||
# ## MANGO_INSTRUCTION_VARIANT_FINDER
|
|
||||||
#
|
|
||||||
# The 'variant' of the instruction is held in the first 4 bytes. The remainder of the data
|
|
||||||
# is per-instruction.
|
|
||||||
#
|
|
||||||
# This `struct` loads only the first 4 bytes, as an `int`, so we know which specific parser
|
|
||||||
# has to be used to load the rest of the data.
|
|
||||||
|
|
||||||
|
|
||||||
MANGO_INSTRUCTION_VARIANT_FINDER = construct.Struct(
|
|
||||||
"variant" / construct.BytesInteger(4, swapped=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ## Variant 0: INIT_MANGO_GROUP
|
|
||||||
#
|
|
||||||
# Instruction variant 1. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs):
|
|
||||||
#
|
|
||||||
# > Initialize a group of lending pools that can be cross margined
|
|
||||||
# ```
|
|
||||||
# InitMangoGroup {
|
|
||||||
# signer_nonce: u64,
|
|
||||||
# maint_coll_ratio: U64F64,
|
|
||||||
# init_coll_ratio: U64F64,
|
|
||||||
# borrow_limits: [u64; NUM_TOKENS]
|
|
||||||
# },
|
|
||||||
# ```
|
|
||||||
|
|
||||||
|
|
||||||
INIT_MANGO_GROUP = construct.Struct(
|
|
||||||
"variant" / construct.Const(0x0, construct.BytesInteger(4, swapped=True)),
|
|
||||||
"signer_nonce" / DecimalAdapter(),
|
|
||||||
"maint_coll_ratio" / FloatAdapter(),
|
|
||||||
"init_coll_ratio" / FloatAdapter(),
|
|
||||||
# "borrow_limits" / construct.Array(NUM_TOKENS, DecimalAdapter()) # This is inconsistently available
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ## Variant 1: INIT_MARGIN_ACCOUNT
|
|
||||||
#
|
|
||||||
# Instruction variant 1. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs):
|
|
||||||
#
|
|
||||||
# > Initialize a margin account for a user
|
|
||||||
# ```
|
|
||||||
# InitMarginAccount,
|
|
||||||
# ```
|
|
||||||
|
|
||||||
|
|
||||||
INIT_MARGIN_ACCOUNT = construct.Struct(
|
|
||||||
"variant" / construct.Const(0x1, construct.BytesInteger(4, swapped=True)),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ## Variant 2: DEPOSIT
|
|
||||||
#
|
|
||||||
# Instruction variant 2. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs):
|
|
||||||
#
|
|
||||||
# > Deposit funds into margin account to be used as collateral and earn interest.
|
|
||||||
# ```
|
|
||||||
# Deposit {
|
|
||||||
# quantity: u64
|
|
||||||
# },
|
|
||||||
# ```
|
|
||||||
|
|
||||||
|
|
||||||
DEPOSIT = construct.Struct(
|
|
||||||
"variant" / construct.Const(0x2, construct.BytesInteger(4, swapped=True)),
|
|
||||||
"quantity" / DecimalAdapter()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ## Variant 3: WITHDRAW
|
|
||||||
#
|
|
||||||
# Instruction variant 3. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs):
|
|
||||||
#
|
|
||||||
# > Withdraw funds that were deposited earlier.
|
|
||||||
# ```
|
|
||||||
# Withdraw {
|
|
||||||
# quantity: u64
|
|
||||||
# },
|
|
||||||
# ```
|
|
||||||
|
|
||||||
|
|
||||||
WITHDRAW = construct.Struct(
|
|
||||||
"variant" / construct.Const(0x3, construct.BytesInteger(4, swapped=True)),
|
|
||||||
"quantity" / DecimalAdapter()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ## Variant 4: BORROW
|
|
||||||
#
|
|
||||||
# Instruction variant 4. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs):
|
|
||||||
#
|
|
||||||
# > Borrow by incrementing MarginAccount.borrows given collateral ratio is below init_coll_rat
|
|
||||||
# ```
|
|
||||||
# Borrow {
|
|
||||||
# token_index: usize,
|
|
||||||
# quantity: u64
|
|
||||||
# },
|
|
||||||
# ```
|
|
||||||
|
|
||||||
|
|
||||||
BORROW = construct.Struct(
|
|
||||||
"variant" / construct.Const(0x4, construct.BytesInteger(4, swapped=True)),
|
|
||||||
"token_index" / DecimalAdapter(),
|
|
||||||
"quantity" / DecimalAdapter()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ## Variant 5: SETTLE_BORROW
|
|
||||||
#
|
|
||||||
# Instruction variant 5. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs):
|
|
||||||
#
|
|
||||||
# > Use this token's position and deposit to reduce borrows
|
|
||||||
# ```
|
|
||||||
# SettleBorrow {
|
|
||||||
# token_index: usize,
|
|
||||||
# quantity: u64
|
|
||||||
# },
|
|
||||||
# ```
|
|
||||||
|
|
||||||
|
|
||||||
SETTLE_BORROW = construct.Struct(
|
|
||||||
"variant" / construct.Const(0x5, construct.BytesInteger(4, swapped=True)),
|
|
||||||
"token_index" / DecimalAdapter(),
|
|
||||||
"quantity" / DecimalAdapter()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ## Variant 6: LIQUIDATE
|
|
||||||
#
|
|
||||||
# Instruction variant 6. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs):
|
|
||||||
#
|
|
||||||
# > Take over a MarginAccount that is below init_coll_ratio by depositing funds
|
|
||||||
# ```
|
|
||||||
# Liquidate {
|
|
||||||
# /// Quantity of each token liquidator is depositing in order to bring account above maint
|
|
||||||
# deposit_quantities: [u64; NUM_TOKENS]
|
|
||||||
# },
|
|
||||||
# ```
|
|
||||||
|
|
||||||
|
|
||||||
_LIQUIDATE_NUM_TOKENS = 3 # Liquidate is deprecated and was only used with 3 tokens.
|
|
||||||
LIQUIDATE = construct.Struct(
|
|
||||||
"variant" / construct.Const(0x6, construct.BytesInteger(4, swapped=True)),
|
|
||||||
"deposit_quantities" / construct.Array(_LIQUIDATE_NUM_TOKENS, DecimalAdapter())
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ## Variant 7: DEPOSIT_SRM
|
|
||||||
#
|
|
||||||
# Instruction variant 7. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs):
|
|
||||||
#
|
|
||||||
# > Deposit SRM into the SRM vault for MangoGroup
|
|
||||||
# >
|
|
||||||
# > These SRM are not at risk and are not counted towards collateral or any margin calculations
|
|
||||||
# >
|
|
||||||
# > Depositing SRM is a strictly altruistic act with no upside and no downside
|
|
||||||
# ```
|
|
||||||
# DepositSrm {
|
|
||||||
# quantity: u64
|
|
||||||
# },
|
|
||||||
# ```
|
|
||||||
|
|
||||||
|
|
||||||
DEPOSIT_SRM = construct.Struct(
|
|
||||||
"variant" / construct.Const(0x7, construct.BytesInteger(4, swapped=True)),
|
|
||||||
"quantity" / DecimalAdapter()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ## Variant 8: WITHDRAW_SRM
|
|
||||||
#
|
|
||||||
# Instruction variant 8. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs):
|
|
||||||
#
|
|
||||||
# > Withdraw SRM owed to this MarginAccount
|
|
||||||
# ```
|
|
||||||
# WithdrawSrm {
|
|
||||||
# quantity: u64
|
|
||||||
# },
|
|
||||||
# ```
|
|
||||||
|
|
||||||
|
|
||||||
WITHDRAW_SRM = construct.Struct(
|
|
||||||
"variant" / construct.Const(0x8, construct.BytesInteger(4, swapped=True)),
|
|
||||||
"quantity" / DecimalAdapter()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ## Variant 9: PLACE_ORDER
|
|
||||||
#
|
|
||||||
# Instruction variant 9. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs):
|
|
||||||
#
|
|
||||||
# > Place an order on the Serum Dex using Mango margin facilities
|
|
||||||
# ```
|
|
||||||
# PlaceOrder {
|
|
||||||
# order: serum_dex::instruction::NewOrderInstructionV3
|
|
||||||
# },
|
|
||||||
# ```
|
|
||||||
|
|
||||||
|
|
||||||
PLACE_ORDER = construct.Struct(
|
|
||||||
"variant" / construct.Const(0x9, construct.BytesInteger(4, swapped=True)),
|
|
||||||
"order" / construct.Padding(1) # Actual type is: serum_dex::instruction::NewOrderInstructionV3
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ## Variant 10: SETTLE_FUNDS
|
|
||||||
#
|
|
||||||
# Instruction variant 10. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs):
|
|
||||||
#
|
|
||||||
# > Settle all funds from serum dex open orders into MarginAccount positions
|
|
||||||
# ```
|
|
||||||
# SettleFunds,
|
|
||||||
# ```
|
|
||||||
|
|
||||||
|
|
||||||
SETTLE_FUNDS = construct.Struct(
|
|
||||||
"variant" / construct.Const(0xa, construct.BytesInteger(4, swapped=True)),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ## Variant 11: CANCEL_ORDER
|
|
||||||
#
|
|
||||||
# Instruction variant 11. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs):
|
|
||||||
#
|
|
||||||
# > Cancel an order using dex instruction
|
|
||||||
# ```
|
|
||||||
# CancelOrder {
|
|
||||||
# order: serum_dex::instruction::CancelOrderInstructionV2
|
|
||||||
# },
|
|
||||||
# ```
|
|
||||||
|
|
||||||
|
|
||||||
CANCEL_ORDER = construct.Struct(
|
|
||||||
"variant" / construct.Const(0xb, construct.BytesInteger(4, swapped=True)),
|
|
||||||
"order" / construct.Padding(1) # Actual type is: serum_dex::instruction::CancelOrderInstructionV2
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ## Variant 12: CANCEL_ORDER_BY_CLIENT_ID
|
|
||||||
#
|
|
||||||
# Instruction variant 12. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs):
|
|
||||||
#
|
|
||||||
# > Cancel an order using client_id
|
|
||||||
# ```
|
|
||||||
# CancelOrderByClientId {
|
|
||||||
# client_id: u64
|
|
||||||
# },
|
|
||||||
# ```
|
|
||||||
|
|
||||||
|
|
||||||
CANCEL_ORDER_BY_CLIENT_ID = construct.Struct(
|
|
||||||
"variant" / construct.Const(0xc, construct.BytesInteger(4, swapped=True)),
|
|
||||||
"client_id" / DecimalAdapter()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ## Variant 13: CHANGE_BORROW_LIMIT
|
|
||||||
#
|
|
||||||
# Instruction variant 13. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs).
|
|
||||||
#
|
|
||||||
# > Change the borrow limit using admin key. This will not affect any open positions on any MarginAccount
|
|
||||||
# >
|
|
||||||
# > This is intended to be an instruction only in alpha stage while liquidity is slowly improved"_
|
|
||||||
# ```
|
|
||||||
# ChangeBorrowLimit {
|
|
||||||
# token_index: usize,
|
|
||||||
# borrow_limit: u64
|
|
||||||
# },
|
|
||||||
# ```
|
|
||||||
|
|
||||||
|
|
||||||
CHANGE_BORROW_LIMIT = construct.Struct(
|
|
||||||
"variant" / construct.Const(0xd, construct.BytesInteger(4, swapped=True)),
|
|
||||||
"token_index" / DecimalAdapter(),
|
|
||||||
"borrow_limit" / DecimalAdapter()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ## Variant 14: PLACE_AND_SETTLE
|
|
||||||
#
|
|
||||||
# Instruction variant 14. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs).
|
|
||||||
#
|
|
||||||
# > Place an order on the Serum Dex and settle funds from the open orders account
|
|
||||||
# ```
|
|
||||||
# PlaceAndSettle {
|
|
||||||
# order: serum_dex::instruction::NewOrderInstructionV3
|
|
||||||
# },
|
|
||||||
# ```
|
|
||||||
|
|
||||||
|
|
||||||
PLACE_AND_SETTLE = construct.Struct(
|
|
||||||
"variant" / construct.Const(0xe, construct.BytesInteger(4, swapped=True)),
|
|
||||||
"order" / construct.Padding(1) # Actual type is: serum_dex::instruction::NewOrderInstructionV3
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ## Variant 15: FORCE_CANCEL_ORDERS
|
|
||||||
#
|
|
||||||
# Instruction variant 15. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs):
|
|
||||||
#
|
|
||||||
# > Allow a liquidator to cancel open orders and settle to recoup funds for partial liquidation
|
|
||||||
#
|
|
||||||
# ```
|
|
||||||
# ForceCancelOrders {
|
|
||||||
# /// Max orders to cancel -- could be useful to lower this if running into compute limits
|
|
||||||
# /// Recommended: 5
|
|
||||||
# limit: u8
|
|
||||||
# },
|
|
||||||
# ```
|
|
||||||
|
|
||||||
|
|
||||||
FORCE_CANCEL_ORDERS = construct.Struct(
|
|
||||||
"variant" / construct.Const(0xf, construct.BytesInteger(4, swapped=True)),
|
|
||||||
"limit" / DecimalAdapter(1)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ## Variant 16: PARTIAL_LIQUIDATE
|
|
||||||
#
|
|
||||||
# Instruction variant 16. From the [Rust source](https://github.com/blockworks-foundation/mango/blob/master/program/src/instruction.rs):
|
|
||||||
#
|
|
||||||
# > Take over a MarginAccount that is below init_coll_ratio by depositing funds
|
|
||||||
#
|
|
||||||
# ```
|
|
||||||
# PartialLiquidate {
|
|
||||||
# /// Quantity of the token being deposited to repay borrows
|
|
||||||
# max_deposit: u64
|
|
||||||
# },
|
|
||||||
# ```
|
|
||||||
|
|
||||||
|
|
||||||
PARTIAL_LIQUIDATE = construct.Struct(
|
|
||||||
"variant" / construct.Const(0x10, construct.BytesInteger(4, swapped=True)),
|
|
||||||
"max_deposit" / DecimalAdapter()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ## InstructionParsersByVariant dictionary
|
|
||||||
#
|
|
||||||
# This dictionary provides an easy way for us to access the specific parser for a given variant.
|
|
||||||
|
|
||||||
|
|
||||||
InstructionParsersByVariant = {
|
|
||||||
0: INIT_MANGO_GROUP,
|
|
||||||
1: INIT_MARGIN_ACCOUNT,
|
|
||||||
2: DEPOSIT,
|
|
||||||
3: WITHDRAW,
|
|
||||||
4: BORROW,
|
|
||||||
5: SETTLE_BORROW,
|
|
||||||
6: LIQUIDATE,
|
|
||||||
7: DEPOSIT_SRM,
|
|
||||||
8: WITHDRAW_SRM,
|
|
||||||
9: PLACE_ORDER,
|
|
||||||
10: SETTLE_FUNDS,
|
|
||||||
11: CANCEL_ORDER,
|
|
||||||
12: CANCEL_ORDER_BY_CLIENT_ID,
|
|
||||||
13: CHANGE_BORROW_LIMIT,
|
|
||||||
14: PLACE_AND_SETTLE,
|
|
||||||
15: FORCE_CANCEL_ORDERS,
|
|
||||||
16: PARTIAL_LIQUIDATE
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
MAX_TOKENS: int = 32
|
MAX_TOKENS: int = 32
|
||||||
MAX_PAIRS: int = MAX_TOKENS - 1
|
MAX_PAIRS: int = MAX_TOKENS - 1
|
||||||
MAX_NODE_BANKS: int = 8
|
MAX_NODE_BANKS: int = 8
|
||||||
|
@ -1128,7 +325,7 @@ QUOTE_INDEX: int = MAX_TOKENS - 1
|
||||||
OPEN_ORDERS_MAX_ORDERS: int = 32
|
OPEN_ORDERS_MAX_ORDERS: int = 32
|
||||||
MAX_BOOK_NODES: int = 1024
|
MAX_BOOK_NODES: int = 1024
|
||||||
|
|
||||||
DATA_TYPE = construct.Enum(construct.Int8ul, Group=0, MarginAccount=1, RootBank=2,
|
DATA_TYPE = construct.Enum(construct.Int8ul, Group=0, Account=1, RootBank=2,
|
||||||
NodeBank=3, PerpMarket=4, Bids=5, Asks=6, Cache=7, EventQueue=8)
|
NodeBank=3, PerpMarket=4, Bids=5, Asks=6, Cache=7, EventQueue=8)
|
||||||
|
|
||||||
METADATA = construct.Struct(
|
METADATA = construct.Struct(
|
||||||
|
@ -1168,7 +365,7 @@ PERP_MARKET_INFO = construct.Struct(
|
||||||
)
|
)
|
||||||
|
|
||||||
# usize is a u64 on Solana, so a regular DecimalAdapter() works
|
# usize is a u64 on Solana, so a regular DecimalAdapter() works
|
||||||
MANGO_GROUP = construct.Struct(
|
GROUP = construct.Struct(
|
||||||
"meta_data" / METADATA,
|
"meta_data" / METADATA,
|
||||||
"num_oracles" / DecimalAdapter(),
|
"num_oracles" / DecimalAdapter(),
|
||||||
"tokens" / construct.Array(MAX_TOKENS, TOKEN_INFO),
|
"tokens" / construct.Array(MAX_TOKENS, TOKEN_INFO),
|
||||||
|
@ -1333,6 +530,20 @@ ORDERBOOK_SIDE = construct.Struct(
|
||||||
"nodes" / construct.Array(MAX_BOOK_NODES, OrderBookNodeAdapter())
|
"nodes" / construct.Array(MAX_BOOK_NODES, OrderBookNodeAdapter())
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# # Instruction Structs
|
||||||
|
|
||||||
|
# ## MANGO_INSTRUCTION_VARIANT_FINDER
|
||||||
|
#
|
||||||
|
# The 'variant' of the instruction is held in the first 4 bytes. The remainder of the data
|
||||||
|
# is per-instruction.
|
||||||
|
#
|
||||||
|
# This `struct` loads only the first 4 bytes, as an `int`, so we know which specific parser
|
||||||
|
# has to be used to load the rest of the data.
|
||||||
|
|
||||||
|
|
||||||
|
MANGO_INSTRUCTION_VARIANT_FINDER = construct.Struct(
|
||||||
|
"variant" / construct.BytesInteger(4, swapped=True)
|
||||||
|
)
|
||||||
|
|
||||||
# /// Place an order on a perp market
|
# /// Place an order on a perp market
|
||||||
# /// Accounts expected by this instruction (6):
|
# /// Accounts expected by this instruction (6):
|
||||||
|
@ -1397,7 +608,7 @@ WITHDRAW_V3 = construct.Struct(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
MerpsInstructionParsersByVariant = {
|
InstructionParsersByVariant = {
|
||||||
0: None, # INIT_MANGO_GROUP,
|
0: None, # INIT_MANGO_GROUP,
|
||||||
1: INIT_MANGO_ACCOUNT, # INIT_MANGO_ACCOUNT,
|
1: INIT_MANGO_ACCOUNT, # INIT_MANGO_ACCOUNT,
|
||||||
2: None, # DEPOSIT,
|
2: None, # DEPOSIT,
|
||||||
|
|
|
@ -20,9 +20,8 @@ import typing
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from .balancesheet import BalanceSheet
|
from .account import Account
|
||||||
from .group import Group
|
from .group import Group
|
||||||
from .marginaccount import MarginAccount
|
|
||||||
from .tokenvalue import TokenValue
|
from .tokenvalue import TokenValue
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,40 +43,14 @@ class LiquidatableState(enum.Flag):
|
||||||
#
|
#
|
||||||
|
|
||||||
class LiquidatableReport:
|
class LiquidatableReport:
|
||||||
def __init__(self, group: Group, prices: typing.List[TokenValue], margin_account: MarginAccount, balance_sheet: BalanceSheet, balances: typing.List[TokenValue], state: LiquidatableState, worthwhile_threshold: Decimal):
|
def __init__(self, group: Group, prices: typing.Sequence[TokenValue], account: Account, state: LiquidatableState, worthwhile_threshold: Decimal):
|
||||||
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
||||||
self.group: Group = group
|
self.group: Group = group
|
||||||
self.prices: typing.List[TokenValue] = prices
|
self.prices: typing.Sequence[TokenValue] = prices
|
||||||
self.margin_account: MarginAccount = margin_account
|
self.account: Account = account
|
||||||
self.balance_sheet: BalanceSheet = balance_sheet
|
|
||||||
self.balances: typing.List[TokenValue] = balances
|
|
||||||
self.state: LiquidatableState = state
|
self.state: LiquidatableState = state
|
||||||
self.worthwhile_threshold: Decimal = worthwhile_threshold
|
self.worthwhile_threshold: Decimal = worthwhile_threshold
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def build(group: Group, prices: typing.List[TokenValue], margin_account: MarginAccount, worthwhile_threshold: Decimal) -> "LiquidatableReport":
|
def build(group: Group, prices: typing.Sequence[TokenValue], account: Account, worthwhile_threshold: Decimal) -> "LiquidatableReport":
|
||||||
balance_sheet = margin_account.get_balance_sheet_totals(group, prices)
|
return LiquidatableReport(group, prices, account, LiquidatableState.UNSET, worthwhile_threshold)
|
||||||
balances = margin_account.get_intrinsic_balances(group)
|
|
||||||
|
|
||||||
state = LiquidatableState.UNSET
|
|
||||||
|
|
||||||
if balance_sheet.collateral_ratio != Decimal(0):
|
|
||||||
if balance_sheet.collateral_ratio <= group.init_coll_ratio:
|
|
||||||
state |= LiquidatableState.RIPE
|
|
||||||
|
|
||||||
if balance_sheet.collateral_ratio <= group.maint_coll_ratio:
|
|
||||||
state |= LiquidatableState.LIQUIDATABLE
|
|
||||||
|
|
||||||
if balance_sheet.collateral_ratio > Decimal(1):
|
|
||||||
state |= LiquidatableState.ABOVE_WATER
|
|
||||||
|
|
||||||
if balance_sheet.assets - balance_sheet.liabilities > worthwhile_threshold:
|
|
||||||
state |= LiquidatableState.WORTHWHILE
|
|
||||||
|
|
||||||
# If a liquidation is ongoing, their account may be above the `maint_coll_ratio` but still
|
|
||||||
# be liquidatable until it reaches `init_coll_ratio`.
|
|
||||||
if margin_account.being_liquidated:
|
|
||||||
state |= LiquidatableState.BEING_LIQUIDATED
|
|
||||||
state |= LiquidatableState.LIQUIDATABLE
|
|
||||||
|
|
||||||
return LiquidatableReport(group, prices, margin_account, balance_sheet, balances, state, worthwhile_threshold)
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ from .tokenvalue import TokenValue
|
||||||
|
|
||||||
|
|
||||||
class LiquidationEvent:
|
class LiquidationEvent:
|
||||||
def __init__(self, timestamp: datetime.datetime, liquidator_name: str, group_name: str, succeeded: bool, signature: str, wallet_address: PublicKey, margin_account_address: PublicKey, balances_before: typing.List[TokenValue], balances_after: typing.List[TokenValue]):
|
def __init__(self, timestamp: datetime.datetime, liquidator_name: str, group_name: str, succeeded: bool, signature: str, wallet_address: PublicKey, margin_account_address: PublicKey, balances_before: typing.Sequence[TokenValue], balances_after: typing.Sequence[TokenValue]):
|
||||||
self.timestamp: datetime.datetime = timestamp
|
self.timestamp: datetime.datetime = timestamp
|
||||||
self.liquidator_name: str = liquidator_name
|
self.liquidator_name: str = liquidator_name
|
||||||
self.group_name: str = group_name
|
self.group_name: str = group_name
|
||||||
|
@ -33,9 +33,9 @@ class LiquidationEvent:
|
||||||
self.signature: str = signature
|
self.signature: str = signature
|
||||||
self.wallet_address: PublicKey = wallet_address
|
self.wallet_address: PublicKey = wallet_address
|
||||||
self.margin_account_address: PublicKey = margin_account_address
|
self.margin_account_address: PublicKey = margin_account_address
|
||||||
self.balances_before: typing.List[TokenValue] = balances_before
|
self.balances_before: typing.Sequence[TokenValue] = balances_before
|
||||||
self.balances_after: typing.List[TokenValue] = balances_after
|
self.balances_after: typing.Sequence[TokenValue] = balances_after
|
||||||
self.changes: typing.List[TokenValue] = TokenValue.changes(balances_before, balances_after)
|
self.changes: typing.Sequence[TokenValue] = TokenValue.changes(balances_before, balances_after)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
result = "✅" if self.succeeded else "❌"
|
result = "✅" if self.succeeded else "❌"
|
||||||
|
|
|
@ -21,12 +21,12 @@ import typing
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from .account import Account
|
||||||
from .accountliquidator import AccountLiquidator
|
from .accountliquidator import AccountLiquidator
|
||||||
from .context import Context
|
from .context import Context
|
||||||
from .group import Group
|
from .group import Group
|
||||||
from .liquidatablereport import LiquidatableReport, LiquidatableState
|
from .liquidatablereport import LiquidatableReport, LiquidatableState
|
||||||
from .liquidationevent import LiquidationEvent
|
from .liquidationevent import LiquidationEvent
|
||||||
from .marginaccount import MarginAccount
|
|
||||||
from .observables import EventSource
|
from .observables import EventSource
|
||||||
from .tokenvalue import TokenValue
|
from .tokenvalue import TokenValue
|
||||||
from .walletbalancer import WalletBalancer
|
from .walletbalancer import WalletBalancer
|
||||||
|
@ -54,8 +54,8 @@ class LiquidationProcessorState(enum.Enum):
|
||||||
|
|
||||||
# # 💧 LiquidationProcessor class
|
# # 💧 LiquidationProcessor class
|
||||||
#
|
#
|
||||||
# An `AccountLiquidator` liquidates a `MarginAccount`. A `LiquidationProcessor` processes a
|
# An `AccountLiquidator` liquidates a `Account`. A `LiquidationProcessor` processes a
|
||||||
# list of `MarginAccount`s, determines if they're liquidatable, and calls an
|
# list of `Account`s, determines if they're liquidatable, and calls an
|
||||||
# `AccountLiquidator` to do the work.
|
# `AccountLiquidator` to do the work.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
@ -72,13 +72,13 @@ class LiquidationProcessor:
|
||||||
self.wallet_balancer: WalletBalancer = wallet_balancer
|
self.wallet_balancer: WalletBalancer = wallet_balancer
|
||||||
self.worthwhile_threshold: Decimal = worthwhile_threshold
|
self.worthwhile_threshold: Decimal = worthwhile_threshold
|
||||||
self.liquidations: EventSource[LiquidationEvent] = EventSource[LiquidationEvent]()
|
self.liquidations: EventSource[LiquidationEvent] = EventSource[LiquidationEvent]()
|
||||||
self.ripe_accounts: typing.Optional[typing.List[MarginAccount]] = None
|
self.ripe_accounts: typing.Optional[typing.Sequence[Account]] = None
|
||||||
self.ripe_accounts_updated_at: datetime = datetime.now()
|
self.ripe_accounts_updated_at: datetime = datetime.now()
|
||||||
self.prices_updated_at: datetime = datetime.now()
|
self.prices_updated_at: datetime = datetime.now()
|
||||||
self.state: LiquidationProcessorState = LiquidationProcessorState.STARTING
|
self.state: LiquidationProcessorState = LiquidationProcessorState.STARTING
|
||||||
self.state_change: EventSource[LiquidationProcessor] = EventSource[LiquidationProcessor]()
|
self.state_change: EventSource[LiquidationProcessor] = EventSource[LiquidationProcessor]()
|
||||||
|
|
||||||
def update_margin_accounts(self, ripe_margin_accounts: typing.List[MarginAccount]):
|
def update_margin_accounts(self, ripe_margin_accounts: typing.Sequence[Account]):
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
f"Received {len(ripe_margin_accounts)} ripe 🥭 margin accounts to process - prices last updated {self.prices_updated_at:%Y-%m-%d %H:%M:%S}")
|
f"Received {len(ripe_margin_accounts)} ripe 🥭 margin accounts to process - prices last updated {self.prices_updated_at:%Y-%m-%d %H:%M:%S}")
|
||||||
self._check_update_recency("prices", self.prices_updated_at)
|
self._check_update_recency("prices", self.prices_updated_at)
|
||||||
|
@ -127,17 +127,19 @@ class LiquidationProcessor:
|
||||||
time_taken = time.time() - started_at
|
time_taken = time.time() - started_at
|
||||||
self.logger.info(f"Check of all ripe 🥭 accounts complete. Time taken: {time_taken:.2f} seconds.")
|
self.logger.info(f"Check of all ripe 🥭 accounts complete. Time taken: {time_taken:.2f} seconds.")
|
||||||
|
|
||||||
def _liquidate_all(self, group: Group, prices: typing.List[TokenValue], to_liquidate: typing.List[LiquidatableReport]):
|
def _liquidate_all(self, group: Group, prices: typing.Sequence[TokenValue], to_liquidate: typing.Sequence[LiquidatableReport]):
|
||||||
to_process = to_liquidate
|
to_process = list(to_liquidate)
|
||||||
while len(to_process) > 0:
|
while len(to_process) > 0:
|
||||||
highest_first = sorted(to_process,
|
# TODO - sort this when LiquidationReport has the proper details for V3.
|
||||||
key=lambda report: report.balance_sheet.assets - report.balance_sheet.liabilities, reverse=True)
|
# highest_first = sorted(to_process,
|
||||||
|
# key=lambda report: report.balance_sheet.assets - report.balance_sheet.liabilities, reverse=True)
|
||||||
|
highest_first = to_process
|
||||||
highest = highest_first[0]
|
highest = highest_first[0]
|
||||||
try:
|
try:
|
||||||
self.account_liquidator.liquidate(highest)
|
self.account_liquidator.liquidate(highest)
|
||||||
self.wallet_balancer.balance(prices)
|
self.wallet_balancer.balance(prices)
|
||||||
|
|
||||||
updated_margin_account = MarginAccount.load(self.context, highest.margin_account.address, group)
|
updated_margin_account = Account.load(self.context, highest.account.address)
|
||||||
updated_report = LiquidatableReport.build(
|
updated_report = LiquidatableReport.build(
|
||||||
group, prices, updated_margin_account, highest.worthwhile_threshold)
|
group, prices, updated_margin_account, highest.worthwhile_threshold)
|
||||||
if not (updated_report.state & LiquidatableState.WORTHWHILE):
|
if not (updated_report.state & LiquidatableState.WORTHWHILE):
|
||||||
|
@ -149,7 +151,7 @@ class LiquidationProcessor:
|
||||||
to_process += [updated_report]
|
to_process += [updated_report]
|
||||||
except Exception as exception:
|
except Exception as exception:
|
||||||
self.logger.error(
|
self.logger.error(
|
||||||
f"Liquidator '{self.name}' - failed to liquidate account '{highest.margin_account.address}' - {exception}.")
|
f"Liquidator '{self.name}' - failed to liquidate account '{highest.account.address}' - {exception}.")
|
||||||
finally:
|
finally:
|
||||||
# highest should always be in to_process, but we're outside the try-except block
|
# highest should always be in to_process, but we're outside the try-except block
|
||||||
# so let's be a little paranoid about it.
|
# so let's be a little paranoid about it.
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
# # ⚠ Warning
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
|
||||||
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
|
||||||
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
||||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
#
|
|
||||||
# [🥭 Mango Markets](https://mango.markets/) support is available at:
|
|
||||||
# [Docs](https://docs.mango.markets/)
|
|
||||||
# [Discord](https://discord.gg/67jySBhxrg)
|
|
||||||
# [Twitter](https://twitter.com/mangomarkets)
|
|
||||||
# [Github](https://github.com/blockworks-foundation)
|
|
||||||
# [Email](mailto:hello@blockworks.foundation)
|
|
||||||
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import typing
|
|
||||||
|
|
||||||
from .version import Version
|
|
||||||
from .layouts import layouts
|
|
||||||
|
|
||||||
|
|
||||||
# # 🥭 MangoAccountFlags class
|
|
||||||
#
|
|
||||||
# The Mango prefix is because there's also `SerumAccountFlags` for the standard Serum flags.
|
|
||||||
#
|
|
||||||
|
|
||||||
class MangoAccountFlags:
|
|
||||||
def __init__(self, version: Version, initialized: bool, group: bool, margin_account: bool, srm_account: bool):
|
|
||||||
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
|
||||||
self.version: Version = version
|
|
||||||
self.initialized = initialized
|
|
||||||
self.group = group
|
|
||||||
self.margin_account = margin_account
|
|
||||||
self.srm_account = srm_account
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_layout(layout: layouts.MANGO_ACCOUNT_FLAGS) -> "MangoAccountFlags":
|
|
||||||
return MangoAccountFlags(Version.UNSPECIFIED, layout.initialized, layout.group,
|
|
||||||
layout.margin_account, layout.srm_account)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
flags: typing.List[typing.Optional[str]] = []
|
|
||||||
flags += ["initialized" if self.initialized else None]
|
|
||||||
flags += ["group" if self.group else None]
|
|
||||||
flags += ["margin_account" if self.margin_account else None]
|
|
||||||
flags += ["srm_account" if self.srm_account else None]
|
|
||||||
flag_text = " | ".join(flag for flag in flags if flag is not None) or "None"
|
|
||||||
return f"« MangoAccountFlags: {flag_text} »"
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"{self}"
|
|
|
@ -1,145 +0,0 @@
|
||||||
# # ⚠ Warning
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
|
||||||
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
|
||||||
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
||||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
#
|
|
||||||
# [🥭 Mango Markets](https://mango.markets/) support is available at:
|
|
||||||
# [Docs](https://docs.mango.markets/)
|
|
||||||
# [Discord](https://discord.gg/67jySBhxrg)
|
|
||||||
# [Twitter](https://twitter.com/mangomarkets)
|
|
||||||
# [Github](https://github.com/blockworks-foundation)
|
|
||||||
# [Email](mailto:hello@blockworks.foundation)
|
|
||||||
|
|
||||||
import typing
|
|
||||||
|
|
||||||
from decimal import Decimal
|
|
||||||
from solana.publickey import PublicKey
|
|
||||||
|
|
||||||
from .accountinfo import AccountInfo
|
|
||||||
from .addressableaccount import AddressableAccount
|
|
||||||
from .context import Context
|
|
||||||
from .layouts import layouts
|
|
||||||
from .marketlookup import MarketLookup
|
|
||||||
from .metadata import Metadata
|
|
||||||
from .perpmarketinfo import PerpMarketInfo
|
|
||||||
from .spotmarketinfo import SpotMarketInfo
|
|
||||||
from .token import SolToken
|
|
||||||
from .tokeninfo import TokenInfo
|
|
||||||
from .tokenlookup import TokenLookup
|
|
||||||
from .tokenvalue import TokenValue
|
|
||||||
from .version import Version
|
|
||||||
|
|
||||||
# # 🥭 MangoGroup class
|
|
||||||
#
|
|
||||||
# `MangoGroup` defines root functionality for Mango Markets.
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
class MangoGroup(AddressableAccount):
|
|
||||||
def __init__(self, account_info: AccountInfo, version: Version, name: str,
|
|
||||||
meta_data: Metadata, tokens: typing.Sequence[typing.Optional[TokenInfo]],
|
|
||||||
spot_markets: typing.Sequence[typing.Optional[SpotMarketInfo]],
|
|
||||||
perp_markets: typing.Sequence[typing.Optional[PerpMarketInfo]],
|
|
||||||
oracles: typing.Sequence[PublicKey], signer_nonce: Decimal, signer_key: PublicKey,
|
|
||||||
admin: PublicKey, dex_program_id: PublicKey, cache: PublicKey, valid_interval: Decimal):
|
|
||||||
super().__init__(account_info)
|
|
||||||
self.version: Version = version
|
|
||||||
self.name: str = name
|
|
||||||
|
|
||||||
self.meta_data: Metadata = meta_data
|
|
||||||
self.tokens: typing.Sequence[typing.Optional[TokenInfo]] = tokens
|
|
||||||
self.spot_markets: typing.Sequence[typing.Optional[SpotMarketInfo]] = spot_markets
|
|
||||||
self.perp_markets: typing.Sequence[typing.Optional[PerpMarketInfo]] = perp_markets
|
|
||||||
self.oracles: typing.Sequence[PublicKey] = oracles
|
|
||||||
self.signer_nonce: Decimal = signer_nonce
|
|
||||||
self.signer_key: PublicKey = signer_key
|
|
||||||
self.admin: PublicKey = admin
|
|
||||||
self.dex_program_id: PublicKey = dex_program_id
|
|
||||||
self.cache: PublicKey = cache
|
|
||||||
self.valid_interval: Decimal = valid_interval
|
|
||||||
|
|
||||||
@property
|
|
||||||
def shared_quote_token(self) -> TokenInfo:
|
|
||||||
quote = self.tokens[-1]
|
|
||||||
if quote is None:
|
|
||||||
raise Exception(f"Could not find shared quote token for group '{self.name}'.")
|
|
||||||
return quote
|
|
||||||
|
|
||||||
@property
|
|
||||||
def base_tokens(self) -> typing.Sequence[typing.Optional[TokenInfo]]:
|
|
||||||
return self.tokens[:-1]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_layout(layout: layouts.MANGO_GROUP, name: str, account_info: AccountInfo, version: Version, token_lookup: TokenLookup, market_lookup: MarketLookup) -> "MangoGroup":
|
|
||||||
meta_data = Metadata.from_layout(layout.meta_data)
|
|
||||||
num_oracles = layout.num_oracles
|
|
||||||
tokens = [TokenInfo.from_layout_or_none(t, token_lookup) for t in layout.tokens]
|
|
||||||
spot_markets = [SpotMarketInfo.from_layout_or_none(m, market_lookup) for m in layout.spot_markets]
|
|
||||||
perp_markets = [PerpMarketInfo.from_layout_or_none(p) for p in layout.perp_markets]
|
|
||||||
oracles = list(layout.oracles)[:int(num_oracles)]
|
|
||||||
signer_nonce = layout.signer_nonce
|
|
||||||
signer_key = layout.signer_key
|
|
||||||
admin = layout.admin
|
|
||||||
dex_program_id = layout.dex_program_id
|
|
||||||
cache = layout.cache
|
|
||||||
valid_interval = layout.valid_interval
|
|
||||||
|
|
||||||
return MangoGroup(account_info, version, name, meta_data, tokens, spot_markets, perp_markets, oracles, signer_nonce, signer_key, admin, dex_program_id, cache, valid_interval)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse(context: Context, account_info: AccountInfo) -> "MangoGroup":
|
|
||||||
data = account_info.data
|
|
||||||
if len(data) != layouts.MANGO_GROUP.sizeof():
|
|
||||||
raise Exception(
|
|
||||||
f"MangoGroup data length ({len(data)}) does not match expected size ({layouts.MANGO_GROUP.sizeof()}")
|
|
||||||
|
|
||||||
layout = layouts.MANGO_GROUP.parse(data)
|
|
||||||
return MangoGroup.from_layout(layout, "merps_test_v3", account_info, Version.V1, context.token_lookup, context.market_lookup)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def load(context: Context, address: typing.Optional[PublicKey] = None) -> "MangoGroup":
|
|
||||||
group_address: PublicKey = address or context.group_id
|
|
||||||
account_info = AccountInfo.load(context, group_address)
|
|
||||||
if account_info is None:
|
|
||||||
raise Exception(f"MangoGroup account not found at address '{group_address}'")
|
|
||||||
return MangoGroup.parse(context, account_info)
|
|
||||||
|
|
||||||
def fetch_balances(self, context: Context, root_address: PublicKey) -> typing.List[TokenValue]:
|
|
||||||
balances: typing.List[TokenValue] = []
|
|
||||||
sol_balance = context.fetch_sol_balance(root_address)
|
|
||||||
balances += [TokenValue(SolToken, sol_balance)]
|
|
||||||
|
|
||||||
for basket_token in self.tokens:
|
|
||||||
if basket_token is not None and basket_token.token is not None:
|
|
||||||
balance = TokenValue.fetch_total_value(context, root_address, basket_token.token)
|
|
||||||
balances += [balance]
|
|
||||||
return balances
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
tokens = "\n ".join([f"{token}".replace("\n", "\n ")
|
|
||||||
for token in self.tokens if token is not None])
|
|
||||||
spot_markets = "\n ".join([f"{spot_market}".replace("\n", "\n ")
|
|
||||||
for spot_market in self.spot_markets if spot_market is not None])
|
|
||||||
perp_markets = "\n ".join([f"{perp_market}".replace("\n", "\n ")
|
|
||||||
for perp_market in self.perp_markets if perp_market is not None])
|
|
||||||
oracles = "\n ".join([f"{oracle}" for oracle in self.oracles])
|
|
||||||
return f"""« 𝙼𝚊𝚗𝚐𝚘𝙶𝚛𝚘𝚞𝚙 {self.version} [{self.address}]
|
|
||||||
{self.meta_data}
|
|
||||||
Name: {self.name}
|
|
||||||
Signer [Nonce: {self.signer_nonce}]: {self.signer_key}
|
|
||||||
Admin: {self.admin}
|
|
||||||
DEX Program ID: {self.dex_program_id}
|
|
||||||
Merps Cache: {self.cache}
|
|
||||||
Valid Interval: {self.valid_interval}
|
|
||||||
Tokens:
|
|
||||||
{tokens}
|
|
||||||
Spot Markets:
|
|
||||||
{spot_markets}
|
|
||||||
Perp Markets:
|
|
||||||
{perp_markets}
|
|
||||||
Oracles:
|
|
||||||
{oracles}
|
|
||||||
»"""
|
|
|
@ -1,393 +0,0 @@
|
||||||
# # ⚠ Warning
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
|
||||||
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
|
||||||
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
||||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
#
|
|
||||||
# [🥭 Mango Markets](https://mango.markets/) support is available at:
|
|
||||||
# [Docs](https://docs.mango.markets/)
|
|
||||||
# [Discord](https://discord.gg/67jySBhxrg)
|
|
||||||
# [Twitter](https://twitter.com/mangomarkets)
|
|
||||||
# [Github](https://github.com/blockworks-foundation)
|
|
||||||
# [Email](mailto:hello@blockworks.foundation)
|
|
||||||
|
|
||||||
|
|
||||||
import construct
|
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
import typing
|
|
||||||
|
|
||||||
from decimal import Decimal
|
|
||||||
from solana.publickey import PublicKey
|
|
||||||
from solana.rpc.types import MemcmpOpts
|
|
||||||
|
|
||||||
from .accountinfo import AccountInfo
|
|
||||||
from .addressableaccount import AddressableAccount
|
|
||||||
from .balancesheet import BalanceSheet
|
|
||||||
from .constants import SYSTEM_PROGRAM_ADDRESS
|
|
||||||
from .context import Context
|
|
||||||
from .encoding import encode_int, encode_key
|
|
||||||
from .group import Group
|
|
||||||
from .layouts import layouts
|
|
||||||
from .mangoaccountflags import MangoAccountFlags
|
|
||||||
from .openorders import OpenOrders
|
|
||||||
from .token import Token
|
|
||||||
from .tokenvalue import TokenValue
|
|
||||||
from .version import Version
|
|
||||||
|
|
||||||
# # 🥭 MarginAccount class
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
class MarginAccount(AddressableAccount):
|
|
||||||
def __init__(self, account_info: AccountInfo, version: Version, account_flags: MangoAccountFlags,
|
|
||||||
has_borrows: bool, mango_group: PublicKey, owner: PublicKey, being_liquidated: bool,
|
|
||||||
deposits: typing.List[TokenValue], borrows: typing.List[TokenValue],
|
|
||||||
open_orders: typing.List[typing.Optional[PublicKey]]):
|
|
||||||
super().__init__(account_info)
|
|
||||||
self.version: Version = version
|
|
||||||
self.account_flags: MangoAccountFlags = account_flags
|
|
||||||
self.has_borrows: bool = has_borrows
|
|
||||||
self.mango_group: PublicKey = mango_group
|
|
||||||
self.owner: PublicKey = owner
|
|
||||||
self.being_liquidated: bool = being_liquidated
|
|
||||||
self.deposits: typing.List[TokenValue] = deposits
|
|
||||||
self.borrows: typing.List[TokenValue] = borrows
|
|
||||||
self.open_orders: typing.List[typing.Optional[PublicKey]] = open_orders
|
|
||||||
self.open_orders_accounts: typing.List[typing.Optional[OpenOrders]] = [None] * len(open_orders)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_layout(layout: construct.Struct, account_info: AccountInfo, version: Version, group: Group) -> "MarginAccount":
|
|
||||||
if group.address != layout.mango_group:
|
|
||||||
raise Exception(
|
|
||||||
f"Margin account belongs to Group ID '{group.address}', not Group ID '{layout.mango_group}'")
|
|
||||||
|
|
||||||
if version == Version.V1:
|
|
||||||
# This is an old-style margin account, with no borrows flag
|
|
||||||
has_borrows = False
|
|
||||||
else:
|
|
||||||
# This is a new-style margin account where we can depend on the presence of the borrows flag
|
|
||||||
has_borrows = bool(layout.has_borrows)
|
|
||||||
|
|
||||||
account_flags: MangoAccountFlags = MangoAccountFlags.from_layout(layout.account_flags)
|
|
||||||
deposits: typing.List[TokenValue] = []
|
|
||||||
for index, deposit in enumerate(layout.deposits):
|
|
||||||
token = group.basket_tokens[index].token
|
|
||||||
rebased_deposit = deposit * group.basket_tokens[index].index.deposit.value
|
|
||||||
token_value = TokenValue(token, rebased_deposit)
|
|
||||||
deposits += [token_value]
|
|
||||||
|
|
||||||
borrows: typing.List[TokenValue] = []
|
|
||||||
for index, borrow in enumerate(layout.borrows):
|
|
||||||
token = group.basket_tokens[index].token
|
|
||||||
rebased_borrow = borrow * group.basket_tokens[index].index.borrow.value
|
|
||||||
token_value = TokenValue(token, rebased_borrow)
|
|
||||||
borrows += [token_value]
|
|
||||||
|
|
||||||
return MarginAccount(account_info, version, account_flags, has_borrows, layout.mango_group,
|
|
||||||
layout.owner, layout.being_liquidated, deposits, borrows, list(layout.open_orders))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse(account_info: AccountInfo, group: Group) -> "MarginAccount":
|
|
||||||
data = account_info.data
|
|
||||||
if len(data) == layouts.MARGIN_ACCOUNT_V1.sizeof():
|
|
||||||
layout = layouts.MARGIN_ACCOUNT_V1.parse(data)
|
|
||||||
version: Version = Version.V1
|
|
||||||
elif len(data) == layouts.MARGIN_ACCOUNT_V2.sizeof():
|
|
||||||
version = Version.V2
|
|
||||||
layout = layouts.MARGIN_ACCOUNT_V2.parse(data)
|
|
||||||
else:
|
|
||||||
raise Exception(
|
|
||||||
f"Data length ({len(data)}) does not match expected size ({layouts.MARGIN_ACCOUNT_V1.sizeof()} or {layouts.MARGIN_ACCOUNT_V2.sizeof()})")
|
|
||||||
|
|
||||||
return MarginAccount.from_layout(layout, account_info, version, group)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def load(context: Context, margin_account_address: PublicKey, group: Group) -> "MarginAccount":
|
|
||||||
account_info = AccountInfo.load(context, margin_account_address)
|
|
||||||
if account_info is None:
|
|
||||||
raise Exception(f"MarginAccount account not found at address '{margin_account_address}'")
|
|
||||||
|
|
||||||
margin_account = MarginAccount.parse(account_info, group)
|
|
||||||
margin_account.load_open_orders_accounts(context, group)
|
|
||||||
return margin_account
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def load_all_for_group(context: Context, program_id: PublicKey, group: Group) -> typing.List["MarginAccount"]:
|
|
||||||
filters = [
|
|
||||||
MemcmpOpts(
|
|
||||||
offset=layouts.MANGO_ACCOUNT_FLAGS.sizeof(), # mango_group is just after the MangoAccountFlags, which is the first entry
|
|
||||||
bytes=encode_key(group.address)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
if group.version == Version.V1:
|
|
||||||
parser = layouts.MARGIN_ACCOUNT_V1
|
|
||||||
else:
|
|
||||||
parser = layouts.MARGIN_ACCOUNT_V2
|
|
||||||
|
|
||||||
response = context.client.get_program_accounts(
|
|
||||||
program_id, data_size=parser.sizeof(), memcmp_opts=filters, commitment=context.commitment, encoding="base64")
|
|
||||||
margin_accounts = []
|
|
||||||
for margin_account_data in response["result"]:
|
|
||||||
address = PublicKey(margin_account_data["pubkey"])
|
|
||||||
account = AccountInfo._from_response_values(margin_account_data["account"], address)
|
|
||||||
margin_account = MarginAccount.parse(account, group)
|
|
||||||
margin_accounts += [margin_account]
|
|
||||||
return margin_accounts
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def load_all_for_group_with_open_orders(context: Context, program_id: PublicKey, group: Group) -> typing.List["MarginAccount"]:
|
|
||||||
margin_accounts = MarginAccount.load_all_for_group(context, program_id, group)
|
|
||||||
open_orders = OpenOrders.load_raw_open_orders_account_infos(context, group)
|
|
||||||
for margin_account in margin_accounts:
|
|
||||||
margin_account.install_open_orders_accounts(group, open_orders)
|
|
||||||
|
|
||||||
return margin_accounts
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def load_all_for_owner(context: Context, owner: PublicKey, group: typing.Optional[Group] = None) -> typing.List["MarginAccount"]:
|
|
||||||
if group is None:
|
|
||||||
group = Group.load(context)
|
|
||||||
|
|
||||||
# mango_group is just after the MangoAccountFlags, which is the first entry.
|
|
||||||
mango_group_offset = layouts.MANGO_ACCOUNT_FLAGS.sizeof()
|
|
||||||
# owner is just after mango_group in the layout, and it's a PublicKey which is 32 bytes.
|
|
||||||
owner_offset = mango_group_offset + 32
|
|
||||||
filters = [
|
|
||||||
MemcmpOpts(
|
|
||||||
offset=mango_group_offset,
|
|
||||||
bytes=encode_key(group.address)
|
|
||||||
),
|
|
||||||
MemcmpOpts(
|
|
||||||
offset=owner_offset,
|
|
||||||
bytes=encode_key(owner)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
response = context.client.get_program_accounts(
|
|
||||||
context.program_id, memcmp_opts=filters, commitment=context.commitment, encoding="base64")
|
|
||||||
margin_accounts = []
|
|
||||||
for margin_account_data in response["result"]:
|
|
||||||
address = PublicKey(margin_account_data["pubkey"])
|
|
||||||
account = AccountInfo._from_response_values(margin_account_data["account"], address)
|
|
||||||
margin_account = MarginAccount.parse(account, group)
|
|
||||||
margin_account.load_open_orders_accounts(context, group)
|
|
||||||
margin_accounts += [margin_account]
|
|
||||||
return margin_accounts
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def filter_out_unripe(cls, margin_accounts: typing.List["MarginAccount"], group: Group, prices: typing.List[TokenValue]) -> typing.List["MarginAccount"]:
|
|
||||||
logger: logging.Logger = logging.getLogger(cls.__name__)
|
|
||||||
|
|
||||||
ripe_accounts: typing.List[MarginAccount] = []
|
|
||||||
for margin_account in margin_accounts:
|
|
||||||
balance_sheet = margin_account.get_balance_sheet_totals(group, prices)
|
|
||||||
if balance_sheet.collateral_ratio > 0:
|
|
||||||
if balance_sheet.collateral_ratio <= group.init_coll_ratio:
|
|
||||||
ripe_accounts += [margin_account]
|
|
||||||
logger.info(
|
|
||||||
f"Of those {len(margin_accounts)}, {len(ripe_accounts)} have a collateral ratio greater than zero but less than the initial collateral ratio of: {group.init_coll_ratio}.")
|
|
||||||
return ripe_accounts
|
|
||||||
|
|
||||||
def load_open_orders_accounts(self, context: Context, group: Group) -> None:
|
|
||||||
for index, oo in enumerate(self.open_orders):
|
|
||||||
key = oo
|
|
||||||
if key is not None:
|
|
||||||
self.open_orders_accounts[index] = OpenOrders.load(
|
|
||||||
context, key, group.basket_tokens[index].token.decimals, group.shared_quote_token.token.decimals)
|
|
||||||
|
|
||||||
def install_open_orders_accounts(self, group: Group, all_open_orders_by_address: typing.Dict[str, AccountInfo]) -> None:
|
|
||||||
for index, oo in enumerate(self.open_orders):
|
|
||||||
key = str(oo)
|
|
||||||
if key in all_open_orders_by_address:
|
|
||||||
open_orders_account_info = all_open_orders_by_address[key]
|
|
||||||
open_orders = OpenOrders.parse(open_orders_account_info,
|
|
||||||
group.basket_tokens[index].token.decimals,
|
|
||||||
group.shared_quote_token.token.decimals)
|
|
||||||
self.open_orders_accounts[index] = open_orders
|
|
||||||
|
|
||||||
def get_intrinsic_balance_sheets(self, group: Group) -> typing.List[BalanceSheet]:
|
|
||||||
settled_assets: typing.List[Decimal] = [Decimal(0)] * len(group.basket_tokens)
|
|
||||||
liabilities: typing.List[Decimal] = [Decimal(0)] * len(group.basket_tokens)
|
|
||||||
for index, token in enumerate(group.basket_tokens):
|
|
||||||
settled_assets[index] = self.deposits[index].value
|
|
||||||
liabilities[index] = self.borrows[index].value
|
|
||||||
|
|
||||||
unsettled_assets: typing.List[Decimal] = [Decimal(0)] * len(group.basket_tokens)
|
|
||||||
for index, open_orders_account in enumerate(self.open_orders_accounts):
|
|
||||||
if open_orders_account is not None:
|
|
||||||
unsettled_assets[index] += open_orders_account.base_token_total
|
|
||||||
unsettled_assets[-1] += open_orders_account.quote_token_total + \
|
|
||||||
open_orders_account.referrer_rebate_accrued
|
|
||||||
|
|
||||||
balance_sheets: typing.List[BalanceSheet] = []
|
|
||||||
for index, token in enumerate(group.basket_tokens):
|
|
||||||
balance_sheets += [BalanceSheet(token.token, liabilities[index],
|
|
||||||
settled_assets[index], unsettled_assets[index])]
|
|
||||||
|
|
||||||
return balance_sheets
|
|
||||||
|
|
||||||
def get_priced_balance_sheets(self, group: Group, prices: typing.List[TokenValue]) -> typing.List[BalanceSheet]:
|
|
||||||
priced: typing.List[BalanceSheet] = []
|
|
||||||
balance_sheets = self.get_intrinsic_balance_sheets(group)
|
|
||||||
for balance_sheet in balance_sheets:
|
|
||||||
price = TokenValue.find_by_token(prices, balance_sheet.token)
|
|
||||||
liabilities = balance_sheet.liabilities * price.value
|
|
||||||
settled_assets = balance_sheet.settled_assets * price.value
|
|
||||||
unsettled_assets = balance_sheet.unsettled_assets * price.value
|
|
||||||
priced += [BalanceSheet(
|
|
||||||
price.token,
|
|
||||||
price.token.round(liabilities),
|
|
||||||
price.token.round(settled_assets),
|
|
||||||
price.token.round(unsettled_assets)
|
|
||||||
)]
|
|
||||||
|
|
||||||
return priced
|
|
||||||
|
|
||||||
def get_balance_sheet_totals(self, group: Group, prices: typing.List[TokenValue]) -> BalanceSheet:
|
|
||||||
liabilities = Decimal(0)
|
|
||||||
settled_assets = Decimal(0)
|
|
||||||
unsettled_assets = Decimal(0)
|
|
||||||
|
|
||||||
balance_sheets = self.get_priced_balance_sheets(group, prices)
|
|
||||||
for balance_sheet in balance_sheets:
|
|
||||||
if balance_sheet is not None:
|
|
||||||
liabilities += balance_sheet.liabilities
|
|
||||||
settled_assets += balance_sheet.settled_assets
|
|
||||||
unsettled_assets += balance_sheet.unsettled_assets
|
|
||||||
|
|
||||||
# A BalanceSheet must have a token - it's a pain to make it a typing.Optional[Token].
|
|
||||||
# So in this one case, we produce a 'fake' token whose symbol is a summary of all token
|
|
||||||
# symbols that went into it.
|
|
||||||
#
|
|
||||||
# If this becomes more painful than typing.Optional[Token], we can go with making
|
|
||||||
# Token optional.
|
|
||||||
summary_name = "-".join([bal.token.name for bal in balance_sheets])
|
|
||||||
summary_token = Token(summary_name, f"{summary_name} Summary", SYSTEM_PROGRAM_ADDRESS, Decimal(0))
|
|
||||||
return BalanceSheet(summary_token, liabilities, settled_assets, unsettled_assets)
|
|
||||||
|
|
||||||
def get_intrinsic_balances(self, group: Group) -> typing.List[TokenValue]:
|
|
||||||
balance_sheets = self.get_intrinsic_balance_sheets(group)
|
|
||||||
balances: typing.List[TokenValue] = []
|
|
||||||
for index, balance_sheet in enumerate(balance_sheets):
|
|
||||||
if balance_sheet.token is None:
|
|
||||||
raise Exception(f"Intrinsic balance sheet with index [{index}] has no token.")
|
|
||||||
balances += [TokenValue(balance_sheet.token, balance_sheet.value)]
|
|
||||||
|
|
||||||
return balances
|
|
||||||
|
|
||||||
# The old way of fetching ripe margin accounts was to fetch them all then inspect them to see
|
|
||||||
# if they were ripe. That was a big performance problem - fetching all groups was quite a penalty.
|
|
||||||
#
|
|
||||||
# This is still how it's done in load_ripe_v1().
|
|
||||||
#
|
|
||||||
# The newer mechanism is to look for the has_borrows flag in the ManrginAccount. That should
|
|
||||||
# mean fewer MarginAccounts need to be fetched.
|
|
||||||
#
|
|
||||||
# This newer method is implemented in load_ripe_v2()
|
|
||||||
@staticmethod
|
|
||||||
def load_ripe(context: Context, group: Group) -> typing.List["MarginAccount"]:
|
|
||||||
if group.version == Version.V1:
|
|
||||||
return MarginAccount._load_ripe_v1(context, group)
|
|
||||||
else:
|
|
||||||
return MarginAccount._load_ripe_v2(context, group)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _load_ripe_v1(cls, context: Context, group: Group) -> typing.List["MarginAccount"]:
|
|
||||||
started_at = time.time()
|
|
||||||
logger: logging.Logger = logging.getLogger(cls.__name__)
|
|
||||||
|
|
||||||
margin_accounts = MarginAccount.load_all_for_group_with_open_orders(context, context.program_id, group)
|
|
||||||
logger.info(f"Fetched {len(margin_accounts)} V1 margin accounts to process.")
|
|
||||||
|
|
||||||
prices = group.fetch_token_prices(context)
|
|
||||||
ripe_accounts = MarginAccount.filter_out_unripe(margin_accounts, group, prices)
|
|
||||||
|
|
||||||
time_taken = time.time() - started_at
|
|
||||||
logger.info(f"Loading ripe 🥭 accounts complete. Time taken: {time_taken:.2f} seconds.")
|
|
||||||
return ripe_accounts
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _load_ripe_v2(cls, context: Context, group: Group) -> typing.List["MarginAccount"]:
|
|
||||||
started_at = time.time()
|
|
||||||
logger: logging.Logger = logging.getLogger(cls.__name__)
|
|
||||||
|
|
||||||
filters = [
|
|
||||||
# 'has_borrows' offset is: 8 + 32 + 32 + (5 * 16) + (5 * 16) + (4 * 32) + 1
|
|
||||||
# = 361
|
|
||||||
MemcmpOpts(
|
|
||||||
offset=361,
|
|
||||||
bytes=encode_int(1)
|
|
||||||
),
|
|
||||||
MemcmpOpts(
|
|
||||||
offset=layouts.MANGO_ACCOUNT_FLAGS.sizeof(), # mango_group is just after the MangoAccountFlags, which is the first entry
|
|
||||||
bytes=encode_key(group.address)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
data_size = layouts.MARGIN_ACCOUNT_V2.sizeof()
|
|
||||||
response = context.client.get_program_accounts(
|
|
||||||
context.program_id, data_size=data_size, memcmp_opts=filters, commitment=context.commitment, encoding="base64")
|
|
||||||
result = context.unwrap_or_raise_exception(response)
|
|
||||||
margin_accounts = []
|
|
||||||
open_orders_addresses = []
|
|
||||||
for margin_account_data in result:
|
|
||||||
address = PublicKey(margin_account_data["pubkey"])
|
|
||||||
account = AccountInfo._from_response_values(margin_account_data["account"], address)
|
|
||||||
margin_account = MarginAccount.parse(account, group)
|
|
||||||
open_orders_addresses += margin_account.open_orders
|
|
||||||
margin_accounts += [margin_account]
|
|
||||||
|
|
||||||
logger.info(f"Fetched {len(margin_accounts)} V2 margin accounts to process.")
|
|
||||||
|
|
||||||
# It looks like this will be more efficient - just specify only the addresses we
|
|
||||||
# need, and install them.
|
|
||||||
#
|
|
||||||
# Unfortunately there's a limit of 100 for the getMultipleAccounts() RPC call,
|
|
||||||
# and doing it repeatedly requires some pauses because of rate limits.
|
|
||||||
#
|
|
||||||
# It's quicker (so far) to bring back every openorders account for the group.
|
|
||||||
#
|
|
||||||
# open_orders_addresses = [oo for oo in open_orders_addresses if oo is not None]
|
|
||||||
|
|
||||||
# open_orders_account_infos = AccountInfo.load_multiple(self.context, open_orders_addresses)
|
|
||||||
# open_orders_account_infos_by_address = {key: value for key, value in [(str(account_info.address), account_info) for account_info in open_orders_account_infos]}
|
|
||||||
|
|
||||||
# for margin_account in margin_accounts:
|
|
||||||
# margin_account.install_open_orders_accounts(self, open_orders_account_infos_by_address)
|
|
||||||
|
|
||||||
# This just fetches every openorder account for the group.
|
|
||||||
open_orders = OpenOrders.load_raw_open_orders_account_infos(context, group)
|
|
||||||
logger.info(f"Fetched {len(open_orders)} openorders accounts.")
|
|
||||||
for margin_account in margin_accounts:
|
|
||||||
margin_account.install_open_orders_accounts(group, open_orders)
|
|
||||||
|
|
||||||
prices = group.fetch_token_prices(context)
|
|
||||||
ripe_accounts = MarginAccount.filter_out_unripe(margin_accounts, group, prices)
|
|
||||||
|
|
||||||
time_taken = time.time() - started_at
|
|
||||||
logger.info(f"Loading ripe 🥭 accounts complete. Time taken: {time_taken:.2f} seconds.")
|
|
||||||
return ripe_accounts
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
deposits = "\n ".join([f"{item}" for item in self.deposits])
|
|
||||||
borrows = "\n ".join([f"{item:}" for item in self.borrows])
|
|
||||||
if all(oo is None for oo in self.open_orders_accounts):
|
|
||||||
open_orders = f"{self.open_orders}"
|
|
||||||
else:
|
|
||||||
open_orders_unindented = f"{self.open_orders_accounts}"
|
|
||||||
open_orders = open_orders_unindented.replace("\n", "\n ")
|
|
||||||
return f"""« MarginAccount: {self.address}
|
|
||||||
Flags: {self.account_flags}
|
|
||||||
Has Borrows: {self.has_borrows}
|
|
||||||
Owner: {self.owner}
|
|
||||||
Mango Group: {self.mango_group}
|
|
||||||
Deposits:
|
|
||||||
{deposits}
|
|
||||||
Borrows:
|
|
||||||
{borrows}
|
|
||||||
Mango Open Orders: {open_orders}
|
|
||||||
»"""
|
|
|
@ -128,7 +128,7 @@ class SimpleMarketMaker:
|
||||||
for order in orders:
|
for order in orders:
|
||||||
self.market_operations.cancel_order(order)
|
self.market_operations.cancel_order(order)
|
||||||
|
|
||||||
def fetch_inventory(self) -> typing.List[mango.TokenValue]:
|
def fetch_inventory(self) -> typing.Sequence[mango.TokenValue]:
|
||||||
return [
|
return [
|
||||||
mango.TokenValue.fetch_total_value(self.context, self.wallet.address, self.market.base),
|
mango.TokenValue.fetch_total_value(self.context, self.wallet.address, self.market.base),
|
||||||
mango.TokenValue.fetch_total_value(self.context, self.wallet.address, self.market.quote)
|
mango.TokenValue.fetch_total_value(self.context, self.wallet.address, self.market.quote)
|
||||||
|
@ -140,7 +140,7 @@ class SimpleMarketMaker:
|
||||||
|
|
||||||
return (bid, ask)
|
return (bid, ask)
|
||||||
|
|
||||||
def calculate_order_sizes(self, price: mango.Price, inventory: typing.List[mango.TokenValue]):
|
def calculate_order_sizes(self, price: mango.Price, inventory: typing.Sequence[mango.TokenValue]):
|
||||||
base_tokens: typing.Optional[mango.TokenValue] = mango.TokenValue.find_by_token(inventory, price.market.base)
|
base_tokens: typing.Optional[mango.TokenValue] = mango.TokenValue.find_by_token(inventory, price.market.base)
|
||||||
if base_tokens is None:
|
if base_tokens is None:
|
||||||
raise Exception(f"Could not find market-maker base token {price.market.base.symbol} in inventory.")
|
raise Exception(f"Could not find market-maker base token {price.market.base.symbol} in inventory.")
|
||||||
|
@ -149,7 +149,7 @@ class SimpleMarketMaker:
|
||||||
sell_size = base_tokens.value * self.position_size_ratio
|
sell_size = base_tokens.value * self.position_size_ratio
|
||||||
return (buy_size, sell_size)
|
return (buy_size, sell_size)
|
||||||
|
|
||||||
def orders_require_action(self, orders: typing.List[mango.Order], price: Decimal, size: Decimal) -> bool:
|
def orders_require_action(self, orders: typing.Sequence[mango.Order], price: Decimal, size: Decimal) -> bool:
|
||||||
# for order in orders:
|
# for order in orders:
|
||||||
# price_tolerance = order.price * self.existing_order_tolerance
|
# price_tolerance = order.price * self.existing_order_tolerance
|
||||||
# size_tolerance = order.size * self.existing_order_tolerance
|
# size_tolerance = order.size * self.existing_order_tolerance
|
||||||
|
|
|
@ -1,64 +0,0 @@
|
||||||
# # ⚠ Warning
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
|
||||||
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
|
||||||
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
||||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
#
|
|
||||||
# [🥭 Mango Markets](https://mango.markets/) support is available at:
|
|
||||||
# [Docs](https://docs.mango.markets/)
|
|
||||||
# [Discord](https://discord.gg/67jySBhxrg)
|
|
||||||
# [Twitter](https://twitter.com/mangomarkets)
|
|
||||||
# [Github](https://github.com/blockworks-foundation)
|
|
||||||
# [Email](mailto:hello@blockworks.foundation)
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from decimal import Decimal
|
|
||||||
from pyserum.market import Market as PySerumMarket
|
|
||||||
from solana.publickey import PublicKey
|
|
||||||
|
|
||||||
from .baskettoken import BasketToken
|
|
||||||
from .context import Context
|
|
||||||
from .market import Market
|
|
||||||
from .spotmarket import SpotMarket
|
|
||||||
|
|
||||||
# # 🥭 MarketMetadata class
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
class MarketMetadata:
|
|
||||||
def __init__(self, name: str, address: PublicKey, base: BasketToken, quote: BasketToken,
|
|
||||||
spot: Market, oracle: PublicKey, decimals: Decimal):
|
|
||||||
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
|
||||||
self.name: str = name
|
|
||||||
self.address: PublicKey = address
|
|
||||||
self.base: BasketToken = base
|
|
||||||
self.quote: BasketToken = quote
|
|
||||||
|
|
||||||
if not isinstance(spot, SpotMarket):
|
|
||||||
raise Exception(f"Spot '{spot}' is not a spot market.")
|
|
||||||
self.spot: SpotMarket = spot
|
|
||||||
self.oracle: PublicKey = oracle
|
|
||||||
self.decimals: Decimal = decimals
|
|
||||||
self.symbol: str = f"{base.token.symbol}/{quote.token.symbol}"
|
|
||||||
self._market = None
|
|
||||||
|
|
||||||
def fetch_market(self, context: Context) -> PySerumMarket:
|
|
||||||
if self._market is None:
|
|
||||||
self._market = PySerumMarket.load(context.client, self.spot.address)
|
|
||||||
|
|
||||||
return self._market
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
base = f"{self.base}".replace("\n", "\n ")
|
|
||||||
quote = f"{self.quote}".replace("\n", "\n ")
|
|
||||||
return f"""« MarketMetadata '{self.name}' [{self.address}/{self.spot.address}]:
|
|
||||||
Base: {base}
|
|
||||||
Quote: {quote}
|
|
||||||
Oracle: {self.oracle} ({self.decimals} decimals)
|
|
||||||
»"""
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"{self}"
|
|
|
@ -64,11 +64,11 @@ class MarketOperations(metaclass=abc.ABCMeta):
|
||||||
raise NotImplementedError("MarketOperations.place_order() is not implemented on the base type.")
|
raise NotImplementedError("MarketOperations.place_order() is not implemented on the base type.")
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def load_orders(self) -> typing.List[Order]:
|
def load_orders(self) -> typing.Sequence[Order]:
|
||||||
raise NotImplementedError("MarketOperations.load_orders() is not implemented on the base type.")
|
raise NotImplementedError("MarketOperations.load_orders() is not implemented on the base type.")
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def load_my_orders(self) -> typing.List[Order]:
|
def load_my_orders(self) -> typing.Sequence[Order]:
|
||||||
raise NotImplementedError("MarketOperations.load_my_orders() is not implemented on the base type.")
|
raise NotImplementedError("MarketOperations.load_my_orders() is not implemented on the base type.")
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
|
@ -99,10 +99,10 @@ class NullMarketOperations(MarketOperations):
|
||||||
self.reporter(report)
|
self.reporter(report)
|
||||||
return Order(id=0, side=side, price=price, size=size, client_id=0, owner=SYSTEM_PROGRAM_ADDRESS)
|
return Order(id=0, side=side, price=price, size=size, client_id=0, owner=SYSTEM_PROGRAM_ADDRESS)
|
||||||
|
|
||||||
def load_orders(self) -> typing.List[Order]:
|
def load_orders(self) -> typing.Sequence[Order]:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def load_my_orders(self) -> typing.List[Order]:
|
def load_my_orders(self) -> typing.Sequence[Order]:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
|
|
|
@ -16,17 +16,17 @@
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from solana.account import Account
|
from solana.account import Account as SolanaAccount
|
||||||
from solana.system_program import CreateAccountParams, create_account
|
from solana.system_program import CreateAccountParams, create_account
|
||||||
from solana.sysvar import SYSVAR_CLOCK_PUBKEY
|
from solana.sysvar import SYSVAR_CLOCK_PUBKEY
|
||||||
from solana.transaction import AccountMeta, TransactionInstruction
|
from solana.transaction import AccountMeta, TransactionInstruction
|
||||||
from spl.token.constants import TOKEN_PROGRAM_ID
|
from spl.token.constants import TOKEN_PROGRAM_ID
|
||||||
|
|
||||||
|
from .account import Account
|
||||||
from .constants import SYSTEM_PROGRAM_ADDRESS
|
from .constants import SYSTEM_PROGRAM_ADDRESS
|
||||||
from .context import Context
|
from .context import Context
|
||||||
|
from .group import Group
|
||||||
from .layouts import layouts
|
from .layouts import layouts
|
||||||
from .mangoaccount import MangoAccount
|
|
||||||
from .mangogroup import MangoGroup
|
|
||||||
from .orders import Order, OrderType, Side
|
from .orders import Order, OrderType, Side
|
||||||
from .perpmarket import PerpMarket
|
from .perpmarket import PerpMarket
|
||||||
from .rootbank import NodeBank, RootBank
|
from .rootbank import NodeBank, RootBank
|
||||||
|
@ -39,7 +39,7 @@ from .wallet import Wallet
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
def build_cancel_perp_order_instructions(context: Context, wallet: Wallet, margin_account: MangoAccount, perp_market: PerpMarket, order: Order) -> typing.Sequence[TransactionInstruction]:
|
def build_cancel_perp_order_instructions(context: Context, wallet: Wallet, margin_account: Account, perp_market: PerpMarket, order: Order) -> typing.Sequence[TransactionInstruction]:
|
||||||
# { buy: 0, sell: 1 }
|
# { buy: 0, sell: 1 }
|
||||||
raw_side: int = 1 if order.side == Side.SELL else 0
|
raw_side: int = 1 if order.side == Side.SELL else 0
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ def build_cancel_perp_order_instructions(context: Context, wallet: Wallet, margi
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def build_place_perp_order_instructions(context: Context, wallet: Wallet, group: MangoGroup, margin_account: MangoAccount, perp_market: PerpMarket, price: Decimal, quantity: Decimal, client_order_id: int, side: Side, order_type: OrderType) -> typing.Sequence[TransactionInstruction]:
|
def build_place_perp_order_instructions(context: Context, wallet: Wallet, group: Group, margin_account: Account, perp_market: PerpMarket, price: Decimal, quantity: Decimal, client_order_id: int, side: Side, order_type: OrderType) -> typing.Sequence[TransactionInstruction]:
|
||||||
# { buy: 0, sell: 1 }
|
# { buy: 0, sell: 1 }
|
||||||
raw_side: int = 1 if side == Side.SELL else 0
|
raw_side: int = 1 if side == Side.SELL else 0
|
||||||
# { limit: 0, ioc: 1, postOnly: 2 }
|
# { limit: 0, ioc: 1, postOnly: 2 }
|
||||||
|
@ -125,14 +125,14 @@ def build_place_perp_order_instructions(context: Context, wallet: Wallet, group:
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def build_create_margin_account_instructions(context: Context, wallet: Wallet, group: MangoGroup, new_account: Account) -> typing.Sequence[TransactionInstruction]:
|
def build_create_account_instructions(context: Context, wallet: Wallet, group: Group, new_account: SolanaAccount) -> typing.Sequence[TransactionInstruction]:
|
||||||
mango_account_address = new_account.public_key()
|
mango_account_address = new_account.public_key()
|
||||||
|
|
||||||
minimum_balance_response = context.client.get_minimum_balance_for_rent_exemption(layouts.MANGO_ACCOUNT.sizeof())
|
minimum_balance_response = context.client.get_minimum_balance_for_rent_exemption(layouts.MANGO_ACCOUNT.sizeof())
|
||||||
minimum_balance = context.unwrap_or_raise_exception(minimum_balance_response)
|
minimum_balance = context.unwrap_or_raise_exception(minimum_balance_response)
|
||||||
create = create_account(
|
create = create_account(
|
||||||
CreateAccountParams(wallet.address, mango_account_address, minimum_balance, layouts.MANGO_ACCOUNT.sizeof(), context.program_id))
|
CreateAccountParams(wallet.address, mango_account_address, minimum_balance, layouts.MANGO_ACCOUNT.sizeof(), context.program_id))
|
||||||
# /// 0. `[]` mango_group_ai - MangoGroup that this mango account is for
|
# /// 0. `[]` mango_group_ai - Group that this mango account is for
|
||||||
# /// 1. `[writable]` mango_account_ai - the mango account data
|
# /// 1. `[writable]` mango_account_ai - the mango account data
|
||||||
# /// 2. `[signer]` owner_ai - Solana account of owner of the mango account
|
# /// 2. `[signer]` owner_ai - Solana account of owner of the mango account
|
||||||
# /// 3. `[]` rent_ai - Rent sysvar account
|
# /// 3. `[]` rent_ai - Rent sysvar account
|
||||||
|
@ -169,7 +169,7 @@ def build_create_margin_account_instructions(context: Context, wallet: Wallet, g
|
||||||
# quantity: u64,
|
# quantity: u64,
|
||||||
# allow_borrow: bool,
|
# allow_borrow: bool,
|
||||||
# },
|
# },
|
||||||
def build_withdraw_instructions(context: Context, wallet: Wallet, group: MangoGroup, margin_account: MangoAccount, root_bank: RootBank, node_bank: NodeBank, token_account: TokenAccount, allow_borrow: bool) -> typing.Sequence[TransactionInstruction]:
|
def build_withdraw_instructions(context: Context, wallet: Wallet, group: Group, margin_account: Account, root_bank: RootBank, node_bank: NodeBank, token_account: TokenAccount, allow_borrow: bool) -> typing.Sequence[TransactionInstruction]:
|
||||||
value = token_account.value.shift_to_native().value
|
value = token_account.value.shift_to_native().value
|
||||||
withdraw = TransactionInstruction(
|
withdraw = TransactionInstruction(
|
||||||
keys=[
|
keys=[
|
||||||
|
|
|
@ -21,13 +21,13 @@ from pyserum.open_orders_account import OpenOrdersAccount
|
||||||
from solana.publickey import PublicKey
|
from solana.publickey import PublicKey
|
||||||
from solana.rpc.types import MemcmpOpts
|
from solana.rpc.types import MemcmpOpts
|
||||||
|
|
||||||
|
from .accountflags import AccountFlags
|
||||||
from .accountinfo import AccountInfo
|
from .accountinfo import AccountInfo
|
||||||
from .addressableaccount import AddressableAccount
|
from .addressableaccount import AddressableAccount
|
||||||
from .context import Context
|
from .context import Context
|
||||||
from .encoding import encode_key
|
from .encoding import encode_key
|
||||||
from .group import Group
|
from .group import Group
|
||||||
from .layouts import layouts
|
from .layouts import layouts
|
||||||
from .serumaccountflags import SerumAccountFlags
|
|
||||||
from .version import Version
|
from .version import Version
|
||||||
|
|
||||||
# # 🥭 OpenOrders class
|
# # 🥭 OpenOrders class
|
||||||
|
@ -36,15 +36,15 @@ from .version import Version
|
||||||
|
|
||||||
class OpenOrders(AddressableAccount):
|
class OpenOrders(AddressableAccount):
|
||||||
def __init__(self, account_info: AccountInfo, version: Version, program_id: PublicKey,
|
def __init__(self, account_info: AccountInfo, version: Version, program_id: PublicKey,
|
||||||
account_flags: SerumAccountFlags, market: PublicKey, owner: PublicKey,
|
account_flags: AccountFlags, market: PublicKey, owner: PublicKey,
|
||||||
base_token_free: Decimal, base_token_total: Decimal, quote_token_free: Decimal,
|
base_token_free: Decimal, base_token_total: Decimal, quote_token_free: Decimal,
|
||||||
quote_token_total: Decimal, free_slot_bits: Decimal, is_bid_bits: Decimal,
|
quote_token_total: Decimal, free_slot_bits: Decimal, is_bid_bits: Decimal,
|
||||||
orders: typing.List[Decimal], client_ids: typing.List[Decimal],
|
orders: typing.Sequence[Decimal], client_ids: typing.Sequence[Decimal],
|
||||||
referrer_rebate_accrued: Decimal):
|
referrer_rebate_accrued: Decimal):
|
||||||
super().__init__(account_info)
|
super().__init__(account_info)
|
||||||
self.version: Version = version
|
self.version: Version = version
|
||||||
self.program_id: PublicKey = program_id
|
self.program_id: PublicKey = program_id
|
||||||
self.account_flags: SerumAccountFlags = account_flags
|
self.account_flags: AccountFlags = account_flags
|
||||||
self.market: PublicKey = market
|
self.market: PublicKey = market
|
||||||
self.owner: PublicKey = owner
|
self.owner: PublicKey = owner
|
||||||
self.base_token_free: Decimal = base_token_free
|
self.base_token_free: Decimal = base_token_free
|
||||||
|
@ -53,8 +53,8 @@ class OpenOrders(AddressableAccount):
|
||||||
self.quote_token_total: Decimal = quote_token_total
|
self.quote_token_total: Decimal = quote_token_total
|
||||||
self.free_slot_bits: Decimal = free_slot_bits
|
self.free_slot_bits: Decimal = free_slot_bits
|
||||||
self.is_bid_bits: Decimal = is_bid_bits
|
self.is_bid_bits: Decimal = is_bid_bits
|
||||||
self.orders: typing.List[Decimal] = orders
|
self.orders: typing.Sequence[Decimal] = orders
|
||||||
self.client_ids: typing.List[Decimal] = client_ids
|
self.client_ids: typing.Sequence[Decimal] = client_ids
|
||||||
self.referrer_rebate_accrued: Decimal = referrer_rebate_accrued
|
self.referrer_rebate_accrued: Decimal = referrer_rebate_accrued
|
||||||
|
|
||||||
# Sometimes pyserum wants to take its own OpenOrdersAccount as a parameter (e.g. in settle_funds())
|
# Sometimes pyserum wants to take its own OpenOrdersAccount as a parameter (e.g. in settle_funds())
|
||||||
|
@ -64,7 +64,7 @@ class OpenOrders(AddressableAccount):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_layout(layout: layouts.OPEN_ORDERS, account_info: AccountInfo,
|
def from_layout(layout: layouts.OPEN_ORDERS, account_info: AccountInfo,
|
||||||
base_decimals: Decimal, quote_decimals: Decimal) -> "OpenOrders":
|
base_decimals: Decimal, quote_decimals: Decimal) -> "OpenOrders":
|
||||||
account_flags = SerumAccountFlags.from_layout(layout.account_flags)
|
account_flags = AccountFlags.from_layout(layout.account_flags)
|
||||||
program_id = account_info.owner
|
program_id = account_info.owner
|
||||||
|
|
||||||
base_divisor = 10 ** base_decimals
|
base_divisor = 10 ** base_decimals
|
||||||
|
@ -73,8 +73,8 @@ class OpenOrders(AddressableAccount):
|
||||||
base_token_total: Decimal = layout.base_token_total / base_divisor
|
base_token_total: Decimal = layout.base_token_total / base_divisor
|
||||||
quote_token_free: Decimal = layout.quote_token_free / quote_divisor
|
quote_token_free: Decimal = layout.quote_token_free / quote_divisor
|
||||||
quote_token_total: Decimal = layout.quote_token_total / quote_divisor
|
quote_token_total: Decimal = layout.quote_token_total / quote_divisor
|
||||||
nonzero_orders: typing.List[Decimal] = list([order for order in layout.orders if order != 0])
|
nonzero_orders: typing.Sequence[Decimal] = list([order for order in layout.orders if order != 0])
|
||||||
nonzero_client_ids: typing.List[Decimal] = list(
|
nonzero_client_ids: typing.Sequence[Decimal] = list(
|
||||||
[client_id for client_id in layout.client_ids if client_id != 0])
|
[client_id for client_id in layout.client_ids if client_id != 0])
|
||||||
|
|
||||||
return OpenOrders(account_info, Version.UNSPECIFIED, program_id, account_flags, layout.market,
|
return OpenOrders(account_info, Version.UNSPECIFIED, program_id, account_flags, layout.market,
|
||||||
|
@ -95,7 +95,7 @@ class OpenOrders(AddressableAccount):
|
||||||
def load_raw_open_orders_account_infos(context: Context, group: Group) -> typing.Dict[str, AccountInfo]:
|
def load_raw_open_orders_account_infos(context: Context, group: Group) -> typing.Dict[str, AccountInfo]:
|
||||||
filters = [
|
filters = [
|
||||||
MemcmpOpts(
|
MemcmpOpts(
|
||||||
offset=layouts.SERUM_ACCOUNT_FLAGS.sizeof() + 37,
|
offset=layouts.ACCOUNT_FLAGS.sizeof() + 37,
|
||||||
bytes=encode_key(group.signer_key)
|
bytes=encode_key(group.signer_key)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
@ -119,11 +119,11 @@ class OpenOrders(AddressableAccount):
|
||||||
def load_for_market_and_owner(context: Context, market: PublicKey, owner: PublicKey, program_id: PublicKey, base_decimals: Decimal, quote_decimals: Decimal):
|
def load_for_market_and_owner(context: Context, market: PublicKey, owner: PublicKey, program_id: PublicKey, base_decimals: Decimal, quote_decimals: Decimal):
|
||||||
filters = [
|
filters = [
|
||||||
MemcmpOpts(
|
MemcmpOpts(
|
||||||
offset=layouts.SERUM_ACCOUNT_FLAGS.sizeof() + 5,
|
offset=layouts.ACCOUNT_FLAGS.sizeof() + 5,
|
||||||
bytes=encode_key(market)
|
bytes=encode_key(market)
|
||||||
),
|
),
|
||||||
MemcmpOpts(
|
MemcmpOpts(
|
||||||
offset=layouts.SERUM_ACCOUNT_FLAGS.sizeof() + 37,
|
offset=layouts.ACCOUNT_FLAGS.sizeof() + 37,
|
||||||
bytes=encode_key(owner)
|
bytes=encode_key(owner)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
|
@ -120,7 +120,7 @@ class PythOracleProvider(OracleProvider):
|
||||||
fixed_usdt = re.sub('USDT$', 'USD', normalised)
|
fixed_usdt = re.sub('USDT$', 'USD', normalised)
|
||||||
return re.sub('USDC$', 'USD', fixed_usdt)
|
return re.sub('USDC$', 'USD', fixed_usdt)
|
||||||
|
|
||||||
def _pyth_symbol_to_market_symbols(self, symbol: str) -> typing.List[str]:
|
def _pyth_symbol_to_market_symbols(self, symbol: str) -> typing.Sequence[str]:
|
||||||
if symbol.endswith("USD"):
|
if symbol.endswith("USD"):
|
||||||
return [f"{symbol}C", f"{symbol}T"]
|
return [f"{symbol}C", f"{symbol}T"]
|
||||||
return [symbol]
|
return [symbol]
|
||||||
|
@ -141,7 +141,7 @@ class PythOracleProvider(OracleProvider):
|
||||||
|
|
||||||
return mapping
|
return mapping
|
||||||
|
|
||||||
def _fetch_all_pyth_products(self, context: Context, address: PublicKey) -> typing.List[typing.Any]:
|
def _fetch_all_pyth_products(self, context: Context, address: PublicKey) -> typing.Sequence[typing.Any]:
|
||||||
mapping = self._load_pyth_mapping(context, address)
|
mapping = self._load_pyth_mapping(context, address)
|
||||||
all_product_addresses = mapping.products[0:int(mapping.num)]
|
all_product_addresses = mapping.products[0:int(mapping.num)]
|
||||||
product_account_infos = AccountInfo.load_multiple(context, all_product_addresses)
|
product_account_infos = AccountInfo.load_multiple(context, all_product_addresses)
|
||||||
|
|
|
@ -35,7 +35,7 @@ class OwnedTokenValue:
|
||||||
self.token_value = token_value
|
self.token_value = token_value
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_by_owner(values: typing.List["OwnedTokenValue"], owner: PublicKey) -> "OwnedTokenValue":
|
def find_by_owner(values: typing.Sequence["OwnedTokenValue"], owner: PublicKey) -> "OwnedTokenValue":
|
||||||
found = [value for value in values if value.owner == owner]
|
found = [value for value in values if value.owner == owner]
|
||||||
if len(found) == 0:
|
if len(found) == 0:
|
||||||
raise Exception(f"Owner '{owner}' not found in: {values}")
|
raise Exception(f"Owner '{owner}' not found in: {values}")
|
||||||
|
@ -46,7 +46,7 @@ class OwnedTokenValue:
|
||||||
return found[0]
|
return found[0]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def changes(before: typing.List["OwnedTokenValue"], after: typing.List["OwnedTokenValue"]) -> typing.List["OwnedTokenValue"]:
|
def changes(before: typing.Sequence["OwnedTokenValue"], after: typing.Sequence["OwnedTokenValue"]) -> typing.Sequence["OwnedTokenValue"]:
|
||||||
changes: typing.List[OwnedTokenValue] = []
|
changes: typing.List[OwnedTokenValue] = []
|
||||||
for before_value in before:
|
for before_value in before:
|
||||||
after_value = OwnedTokenValue.find_by_owner(after, before_value.owner)
|
after_value = OwnedTokenValue.find_by_owner(after, before_value.owner)
|
||||||
|
|
|
@ -20,8 +20,8 @@ from solana.publickey import PublicKey
|
||||||
from .accountinfo import AccountInfo
|
from .accountinfo import AccountInfo
|
||||||
from .addressableaccount import AddressableAccount
|
from .addressableaccount import AddressableAccount
|
||||||
from .context import Context
|
from .context import Context
|
||||||
|
from .group import Group
|
||||||
from .layouts import layouts
|
from .layouts import layouts
|
||||||
from .mangogroup import MangoGroup
|
|
||||||
from .metadata import Metadata
|
from .metadata import Metadata
|
||||||
from .tokeninfo import TokenInfo
|
from .tokeninfo import TokenInfo
|
||||||
from .version import Version
|
from .version import Version
|
||||||
|
@ -34,7 +34,7 @@ from .version import Version
|
||||||
|
|
||||||
class PerpMarket(AddressableAccount):
|
class PerpMarket(AddressableAccount):
|
||||||
def __init__(self, account_info: AccountInfo, version: Version,
|
def __init__(self, account_info: AccountInfo, version: Version,
|
||||||
meta_data: Metadata, group: MangoGroup, bids: PublicKey, asks: PublicKey,
|
meta_data: Metadata, group: Group, bids: PublicKey, asks: PublicKey,
|
||||||
event_queue: PublicKey, long_funding: Decimal, short_funding: Decimal,
|
event_queue: PublicKey, long_funding: Decimal, short_funding: Decimal,
|
||||||
open_interest: Decimal, quote_lot_size: Decimal, index_oracle: PublicKey,
|
open_interest: Decimal, quote_lot_size: Decimal, index_oracle: PublicKey,
|
||||||
last_updated: datetime, seq_num: Decimal, contract_size: Decimal
|
last_updated: datetime, seq_num: Decimal, contract_size: Decimal
|
||||||
|
@ -43,7 +43,7 @@ class PerpMarket(AddressableAccount):
|
||||||
self.version: Version = version
|
self.version: Version = version
|
||||||
|
|
||||||
self.meta_data: Metadata = meta_data
|
self.meta_data: Metadata = meta_data
|
||||||
self.group: MangoGroup = group
|
self.group: Group = group
|
||||||
self.bids: PublicKey = bids
|
self.bids: PublicKey = bids
|
||||||
self.asks: PublicKey = asks
|
self.asks: PublicKey = asks
|
||||||
self.event_queue: PublicKey = event_queue
|
self.event_queue: PublicKey = event_queue
|
||||||
|
@ -74,7 +74,7 @@ class PerpMarket(AddressableAccount):
|
||||||
self.quote_token: TokenInfo = quote_token
|
self.quote_token: TokenInfo = quote_token
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_layout(layout: layouts.PERP_MARKET, account_info: AccountInfo, version: Version, group: MangoGroup) -> "PerpMarket":
|
def from_layout(layout: layouts.PERP_MARKET, account_info: AccountInfo, version: Version, group: Group) -> "PerpMarket":
|
||||||
meta_data = Metadata.from_layout(layout.meta_data)
|
meta_data = Metadata.from_layout(layout.meta_data)
|
||||||
bids: PublicKey = layout.bids
|
bids: PublicKey = layout.bids
|
||||||
asks: PublicKey = layout.asks
|
asks: PublicKey = layout.asks
|
||||||
|
@ -91,7 +91,7 @@ class PerpMarket(AddressableAccount):
|
||||||
return PerpMarket(account_info, version, meta_data, group, bids, asks, event_queue, long_funding, short_funding, open_interest, quote_lot_size, index_oracle, last_updated, seq_num, contract_size)
|
return PerpMarket(account_info, version, meta_data, group, bids, asks, event_queue, long_funding, short_funding, open_interest, quote_lot_size, index_oracle, last_updated, seq_num, contract_size)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse(account_info: AccountInfo, group: MangoGroup) -> "PerpMarket":
|
def parse(account_info: AccountInfo, group: Group) -> "PerpMarket":
|
||||||
data = account_info.data
|
data = account_info.data
|
||||||
if len(data) != layouts.PERP_MARKET.sizeof():
|
if len(data) != layouts.PERP_MARKET.sizeof():
|
||||||
raise Exception(
|
raise Exception(
|
||||||
|
@ -101,7 +101,7 @@ class PerpMarket(AddressableAccount):
|
||||||
return PerpMarket.from_layout(layout, account_info, Version.V1, group)
|
return PerpMarket.from_layout(layout, account_info, Version.V1, group)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load(context: Context, group: MangoGroup, address: PublicKey) -> "PerpMarket":
|
def load(context: Context, group: Group, address: PublicKey) -> "PerpMarket":
|
||||||
account_info = AccountInfo.load(context, address)
|
account_info = AccountInfo.load(context, address)
|
||||||
if account_info is None:
|
if account_info is None:
|
||||||
raise Exception(f"PerpMarket account not found at address '{address}'")
|
raise Exception(f"PerpMarket account not found at address '{address}'")
|
||||||
|
|
|
@ -17,13 +17,13 @@
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from solana.account import Account
|
from solana.account import Account as SolanaAccount
|
||||||
from solana.publickey import PublicKey
|
from solana.publickey import PublicKey
|
||||||
from solana.transaction import Transaction
|
from solana.transaction import Transaction
|
||||||
|
|
||||||
|
from .account import Account
|
||||||
from .accountinfo import AccountInfo
|
from .accountinfo import AccountInfo
|
||||||
from .context import Context
|
from .context import Context
|
||||||
from .mangoaccount import MangoAccount
|
|
||||||
from .marketoperations import MarketOperations
|
from .marketoperations import MarketOperations
|
||||||
from .merpsinstructions import build_cancel_perp_order_instructions, build_place_perp_order_instructions
|
from .merpsinstructions import build_cancel_perp_order_instructions, build_place_perp_order_instructions
|
||||||
from .orderbookside import OrderBookSide
|
from .orderbookside import OrderBookSide
|
||||||
|
@ -40,13 +40,13 @@ from .wallet import Wallet
|
||||||
|
|
||||||
class PerpMarketOperations(MarketOperations):
|
class PerpMarketOperations(MarketOperations):
|
||||||
def __init__(self, market_name: str, context: Context, wallet: Wallet,
|
def __init__(self, market_name: str, context: Context, wallet: Wallet,
|
||||||
margin_account: MangoAccount, perp_market: PerpMarket,
|
margin_account: Account, perp_market: PerpMarket,
|
||||||
reporter: typing.Callable[[str], None] = None):
|
reporter: typing.Callable[[str], None] = None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.market_name: str = market_name
|
self.market_name: str = market_name
|
||||||
self.context: Context = context
|
self.context: Context = context
|
||||||
self.wallet: Wallet = wallet
|
self.wallet: Wallet = wallet
|
||||||
self.margin_account: MangoAccount = margin_account
|
self.margin_account: Account = margin_account
|
||||||
self.perp_market: PerpMarket = perp_market
|
self.perp_market: PerpMarket = perp_market
|
||||||
self.reporter = reporter or (lambda _: None)
|
self.reporter = reporter or (lambda _: None)
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ class PerpMarketOperations(MarketOperations):
|
||||||
self.logger.info(report)
|
self.logger.info(report)
|
||||||
self.reporter(report)
|
self.reporter(report)
|
||||||
|
|
||||||
signers: typing.List[Account] = [self.wallet.account]
|
signers: typing.Sequence[SolanaAccount] = [self.wallet.account]
|
||||||
transaction = Transaction()
|
transaction = Transaction()
|
||||||
cancel_instructions = build_cancel_perp_order_instructions(
|
cancel_instructions = build_cancel_perp_order_instructions(
|
||||||
self.context, self.wallet, self.margin_account, self.perp_market, order)
|
self.context, self.wallet, self.margin_account, self.perp_market, order)
|
||||||
|
@ -69,7 +69,7 @@ class PerpMarketOperations(MarketOperations):
|
||||||
self.logger.info(report)
|
self.logger.info(report)
|
||||||
self.reporter(report)
|
self.reporter(report)
|
||||||
|
|
||||||
signers: typing.List[Account] = [self.wallet.account]
|
signers: typing.Sequence[SolanaAccount] = [self.wallet.account]
|
||||||
transaction = Transaction()
|
transaction = Transaction()
|
||||||
place_instructions = build_place_perp_order_instructions(
|
place_instructions = build_place_perp_order_instructions(
|
||||||
self.context, self.wallet, self.perp_market.group, self.margin_account, self.perp_market, price, size, client_order_id, side, order_type)
|
self.context, self.wallet, self.perp_market.group, self.margin_account, self.perp_market, price, size, client_order_id, side, order_type)
|
||||||
|
@ -79,7 +79,7 @@ class PerpMarketOperations(MarketOperations):
|
||||||
|
|
||||||
return Order(id=0, side=side, price=price, size=size, client_id=client_order_id, owner=self.margin_account.address)
|
return Order(id=0, side=side, price=price, size=size, client_id=client_order_id, owner=self.margin_account.address)
|
||||||
|
|
||||||
def load_orders(self) -> typing.List[Order]:
|
def load_orders(self) -> typing.Sequence[Order]:
|
||||||
bids_address: PublicKey = self.perp_market.bids
|
bids_address: PublicKey = self.perp_market.bids
|
||||||
asks_address: PublicKey = self.perp_market.asks
|
asks_address: PublicKey = self.perp_market.asks
|
||||||
[bids, asks] = AccountInfo.load_multiple(self.context, [bids_address, asks_address])
|
[bids, asks] = AccountInfo.load_multiple(self.context, [bids_address, asks_address])
|
||||||
|
@ -88,7 +88,7 @@ class PerpMarketOperations(MarketOperations):
|
||||||
|
|
||||||
return [*bid_side.orders(), *ask_side.orders()]
|
return [*bid_side.orders(), *ask_side.orders()]
|
||||||
|
|
||||||
def load_my_orders(self) -> typing.List[Order]:
|
def load_my_orders(self) -> typing.Sequence[Order]:
|
||||||
orders = self.load_orders()
|
orders = self.load_orders()
|
||||||
mine = []
|
mine = []
|
||||||
for order in orders:
|
for order in orders:
|
||||||
|
|
|
@ -42,11 +42,11 @@ from decimal import Decimal
|
||||||
|
|
||||||
|
|
||||||
class RetryWithPauses:
|
class RetryWithPauses:
|
||||||
def __init__(self, name: str, func: typing.Callable, pauses: typing.List[Decimal]) -> None:
|
def __init__(self, name: str, func: typing.Callable, pauses: typing.Sequence[Decimal]) -> None:
|
||||||
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
||||||
self.name: str = name
|
self.name: str = name
|
||||||
self.func: typing.Callable = func
|
self.func: typing.Callable = func
|
||||||
self.pauses: typing.List[Decimal] = pauses
|
self.pauses: typing.Sequence[Decimal] = pauses
|
||||||
|
|
||||||
def run(self, *args, **kwargs):
|
def run(self, *args, **kwargs):
|
||||||
captured_exception: Exception = None
|
captured_exception: Exception = None
|
||||||
|
@ -98,5 +98,5 @@ class RetryWithPauses:
|
||||||
# ```
|
# ```
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def retry_context(name: str, func: typing.Callable, pauses: typing.List[Decimal]) -> typing.Iterator[RetryWithPauses]:
|
def retry_context(name: str, func: typing.Callable, pauses: typing.Sequence[Decimal]) -> typing.Iterator[RetryWithPauses]:
|
||||||
yield RetryWithPauses(name, func, pauses)
|
yield RetryWithPauses(name, func, pauses)
|
||||||
|
|
|
@ -99,7 +99,7 @@ class SerumMarketOperations(MarketOperations):
|
||||||
client_id=serum_order.client_id, owner=serum_order.open_order_address)
|
client_id=serum_order.client_id, owner=serum_order.open_order_address)
|
||||||
return order
|
return order
|
||||||
|
|
||||||
def load_orders(self) -> typing.List[Order]:
|
def load_orders(self) -> typing.Sequence[Order]:
|
||||||
asks = self.market.load_asks()
|
asks = self.market.load_asks()
|
||||||
orders: typing.List[Order] = []
|
orders: typing.List[Order] = []
|
||||||
for serum_order in asks:
|
for serum_order in asks:
|
||||||
|
@ -111,7 +111,7 @@ class SerumMarketOperations(MarketOperations):
|
||||||
|
|
||||||
return orders
|
return orders
|
||||||
|
|
||||||
def load_my_orders(self) -> typing.List[Order]:
|
def load_my_orders(self) -> typing.Sequence[Order]:
|
||||||
serum_orders = self.market.load_orders_for_owner(self.wallet.address)
|
serum_orders = self.market.load_orders_for_owner(self.wallet.address)
|
||||||
orders: typing.List[Order] = []
|
orders: typing.List[Order] = []
|
||||||
for serum_order in serum_orders:
|
for serum_order in serum_orders:
|
||||||
|
|
|
@ -52,7 +52,7 @@ class Token:
|
||||||
return self.symbol.upper() == symbol.upper()
|
return self.symbol.upper() == symbol.upper()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_by_symbol(values: typing.List["Token"], symbol: str) -> "Token":
|
def find_by_symbol(values: typing.Sequence["Token"], symbol: str) -> "Token":
|
||||||
found = [value for value in values if value.symbol_matches(symbol)]
|
found = [value for value in values if value.symbol_matches(symbol)]
|
||||||
if len(found) == 0:
|
if len(found) == 0:
|
||||||
raise Exception(f"Token '{symbol}' not found in token values: {values}")
|
raise Exception(f"Token '{symbol}' not found in token values: {values}")
|
||||||
|
@ -63,7 +63,7 @@ class Token:
|
||||||
return found[0]
|
return found[0]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_by_mint(values: typing.List["Token"], mint: PublicKey) -> "Token":
|
def find_by_mint(values: typing.Sequence["Token"], mint: PublicKey) -> "Token":
|
||||||
found = [value for value in values if value.mint == mint]
|
found = [value for value in values if value.mint == mint]
|
||||||
if len(found) == 0:
|
if len(found) == 0:
|
||||||
raise Exception(f"Token '{mint}' not found in token values: {values}")
|
raise Exception(f"Token '{mint}' not found in token values: {values}")
|
||||||
|
|
|
@ -30,19 +30,21 @@ from .tokenlookup import TokenLookup
|
||||||
|
|
||||||
|
|
||||||
class TokenInfo():
|
class TokenInfo():
|
||||||
def __init__(self, token: typing.Optional[Token], root_bank: PublicKey, decimals: Decimal):
|
def __init__(self, token: Token, root_bank: PublicKey, decimals: Decimal):
|
||||||
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
||||||
self.token: typing.Optional[Token] = token
|
self.token: Token = token
|
||||||
self.root_bank: PublicKey = root_bank
|
self.root_bank: PublicKey = root_bank
|
||||||
self.decimals: Decimal = decimals
|
self.decimals: Decimal = decimals
|
||||||
|
|
||||||
def from_layout(layout: layouts.TOKEN_INFO, token_lookup: TokenLookup) -> "TokenInfo":
|
def from_layout(layout: layouts.TOKEN_INFO, token_lookup: TokenLookup) -> "TokenInfo":
|
||||||
token = token_lookup.find_by_mint(layout.mint)
|
token = token_lookup.find_by_mint(layout.mint)
|
||||||
# TODO - this should be resolved. The decimals should match, but the mixture of token lookups is getting messy.
|
if token is None:
|
||||||
# if token is not None:
|
raise Exception(f"Token with mint {layout.mint} could not be found.")
|
||||||
# if layout.decimals != token.decimals:
|
|
||||||
# raise Exception(
|
if layout.decimals != token.decimals:
|
||||||
# f"Conflict between number of decimals in token static data {token.decimals} and group {layout.decimals} for token {token.symbol}.")
|
raise Exception(
|
||||||
|
f"Conflict between number of decimals in token static data {token.decimals} and group {layout.decimals} for token {token.symbol}.")
|
||||||
|
|
||||||
return TokenInfo(token, layout.root_bank, layout.decimals)
|
return TokenInfo(token, layout.root_bank, layout.decimals)
|
||||||
|
|
||||||
def from_layout_or_none(layout: layouts.TOKEN_INFO, token_lookup: TokenLookup) -> typing.Optional["TokenInfo"]:
|
def from_layout_or_none(layout: layouts.TOKEN_INFO, token_lookup: TokenLookup) -> typing.Optional["TokenInfo"]:
|
||||||
|
|
|
@ -69,12 +69,12 @@ class TokenValue:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def report(reporter: typing.Callable[[str], None], values: typing.List["TokenValue"]) -> None:
|
def report(reporter: typing.Callable[[str], None], values: typing.Sequence["TokenValue"]) -> None:
|
||||||
for value in values:
|
for value in values:
|
||||||
reporter(f"{value.value:>18,.8f} {value.token.name}")
|
reporter(f"{value.value:>18,.8f} {value.token.name}")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_by_symbol(values: typing.List["TokenValue"], symbol: str) -> "TokenValue":
|
def find_by_symbol(values: typing.Sequence["TokenValue"], symbol: str) -> "TokenValue":
|
||||||
found = [value for value in values if value.token.symbol_matches(symbol)]
|
found = [value for value in values if value.token.symbol_matches(symbol)]
|
||||||
if len(found) == 0:
|
if len(found) == 0:
|
||||||
raise Exception(f"Token '{symbol}' not found in token values: {values}")
|
raise Exception(f"Token '{symbol}' not found in token values: {values}")
|
||||||
|
@ -85,7 +85,7 @@ class TokenValue:
|
||||||
return found[0]
|
return found[0]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_by_mint(values: typing.List["TokenValue"], mint: PublicKey) -> "TokenValue":
|
def find_by_mint(values: typing.Sequence["TokenValue"], mint: PublicKey) -> "TokenValue":
|
||||||
found = [value for value in values if value.token.mint == mint]
|
found = [value for value in values if value.token.mint == mint]
|
||||||
if len(found) == 0:
|
if len(found) == 0:
|
||||||
raise Exception(f"Token '{mint}' not found in token values: {values}")
|
raise Exception(f"Token '{mint}' not found in token values: {values}")
|
||||||
|
@ -96,11 +96,11 @@ class TokenValue:
|
||||||
return found[0]
|
return found[0]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_by_token(values: typing.List["TokenValue"], token: Token) -> "TokenValue":
|
def find_by_token(values: typing.Sequence["TokenValue"], token: Token) -> "TokenValue":
|
||||||
return TokenValue.find_by_mint(values, token.mint)
|
return TokenValue.find_by_mint(values, token.mint)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def changes(before: typing.List["TokenValue"], after: typing.List["TokenValue"]) -> typing.List["TokenValue"]:
|
def changes(before: typing.Sequence["TokenValue"], after: typing.Sequence["TokenValue"]) -> typing.Sequence["TokenValue"]:
|
||||||
changes: typing.List[TokenValue] = []
|
changes: typing.List[TokenValue] = []
|
||||||
for before_balance in before:
|
for before_balance in before:
|
||||||
after_balance = TokenValue.find_by_token(after, before_balance.token)
|
after_balance = TokenValue.find_by_token(after, before_balance.token)
|
||||||
|
|
|
@ -133,7 +133,7 @@ _token_out_indices: typing.Dict[InstructionType, int] = {
|
||||||
|
|
||||||
|
|
||||||
class MangoInstruction:
|
class MangoInstruction:
|
||||||
def __init__(self, instruction_type: InstructionType, instruction_data: typing.Any, accounts: typing.List[PublicKey]):
|
def __init__(self, instruction_type: InstructionType, instruction_data: typing.Any, accounts: typing.Sequence[PublicKey]):
|
||||||
self.instruction_type = instruction_type
|
self.instruction_type = instruction_type
|
||||||
self.instruction_data = instruction_data
|
self.instruction_data = instruction_data
|
||||||
self.accounts = accounts
|
self.accounts = accounts
|
||||||
|
@ -203,7 +203,7 @@ class MangoInstruction:
|
||||||
return additional_data
|
return additional_data
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_response(context: Context, all_accounts: typing.List[PublicKey], instruction_data: typing.Dict) -> typing.Optional["MangoInstruction"]:
|
def from_response(context: Context, all_accounts: typing.Sequence[PublicKey], instruction_data: typing.Dict) -> typing.Optional["MangoInstruction"]:
|
||||||
program_account_index = instruction_data["programIdIndex"]
|
program_account_index = instruction_data["programIdIndex"]
|
||||||
if all_accounts[program_account_index] != context.program_id:
|
if all_accounts[program_account_index] != context.program_id:
|
||||||
# It's an instruction, it's just not a Mango one.
|
# It's an instruction, it's just not a Mango one.
|
||||||
|
@ -215,6 +215,8 @@ class MangoInstruction:
|
||||||
decoded = base58.b58decode(data)
|
decoded = base58.b58decode(data)
|
||||||
initial = layouts.MANGO_INSTRUCTION_VARIANT_FINDER.parse(decoded)
|
initial = layouts.MANGO_INSTRUCTION_VARIANT_FINDER.parse(decoded)
|
||||||
parser = layouts.InstructionParsersByVariant[initial.variant]
|
parser = layouts.InstructionParsersByVariant[initial.variant]
|
||||||
|
if parser is None:
|
||||||
|
raise Exception(f"Could not find instruction parser for variant {initial.variant}.")
|
||||||
|
|
||||||
# A whole bunch of accounts are listed for a transaction. Some (or all) of them apply
|
# A whole bunch of accounts are listed for a transaction. Some (or all) of them apply
|
||||||
# to this instruction. The instruction data gives the index of each account it uses,
|
# to this instruction. The instruction data gives the index of each account it uses,
|
||||||
|
@ -244,20 +246,20 @@ class MangoInstruction:
|
||||||
|
|
||||||
|
|
||||||
class TransactionScout:
|
class TransactionScout:
|
||||||
def __init__(self, timestamp: datetime.datetime, signatures: typing.List[str],
|
def __init__(self, timestamp: datetime.datetime, signatures: typing.Sequence[str],
|
||||||
succeeded: bool, group_name: str, accounts: typing.List[PublicKey],
|
succeeded: bool, group_name: str, accounts: typing.Sequence[PublicKey],
|
||||||
instructions: typing.List[typing.Any], messages: typing.List[str],
|
instructions: typing.Sequence[typing.Any], messages: typing.Sequence[str],
|
||||||
pre_token_balances: typing.List[OwnedTokenValue],
|
pre_token_balances: typing.Sequence[OwnedTokenValue],
|
||||||
post_token_balances: typing.List[OwnedTokenValue]):
|
post_token_balances: typing.Sequence[OwnedTokenValue]):
|
||||||
self.timestamp: datetime.datetime = timestamp
|
self.timestamp: datetime.datetime = timestamp
|
||||||
self.signatures: typing.List[str] = signatures
|
self.signatures: typing.Sequence[str] = signatures
|
||||||
self.succeeded: bool = succeeded
|
self.succeeded: bool = succeeded
|
||||||
self.group_name: str = group_name
|
self.group_name: str = group_name
|
||||||
self.accounts: typing.List[PublicKey] = accounts
|
self.accounts: typing.Sequence[PublicKey] = accounts
|
||||||
self.instructions: typing.List[typing.Any] = instructions
|
self.instructions: typing.Sequence[typing.Any] = instructions
|
||||||
self.messages: typing.List[str] = messages
|
self.messages: typing.Sequence[str] = messages
|
||||||
self.pre_token_balances: typing.List[OwnedTokenValue] = pre_token_balances
|
self.pre_token_balances: typing.Sequence[OwnedTokenValue] = pre_token_balances
|
||||||
self.post_token_balances: typing.List[OwnedTokenValue] = post_token_balances
|
self.post_token_balances: typing.Sequence[OwnedTokenValue] = post_token_balances
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def summary(self) -> str:
|
def summary(self) -> str:
|
||||||
|
@ -309,7 +311,7 @@ class TransactionScout:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_transaction_response(context: Context, response: typing.Dict) -> "TransactionScout":
|
def from_transaction_response(context: Context, response: typing.Dict) -> "TransactionScout":
|
||||||
def balance_to_token_value(accounts: typing.List[PublicKey], balance: typing.Dict) -> OwnedTokenValue:
|
def balance_to_token_value(accounts: typing.Sequence[PublicKey], balance: typing.Dict) -> OwnedTokenValue:
|
||||||
mint = PublicKey(balance["mint"])
|
mint = PublicKey(balance["mint"])
|
||||||
account = accounts[balance["accountIndex"]]
|
account = accounts[balance["accountIndex"]]
|
||||||
amount = Decimal(balance["uiTokenAmount"]["amount"])
|
amount = Decimal(balance["uiTokenAmount"]["amount"])
|
||||||
|
@ -352,7 +354,7 @@ class TransactionScout:
|
||||||
raise Exception(f"Exception fetching transaction '{signature}'", exception)
|
raise Exception(f"Exception fetching transaction '{signature}'", exception)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
def format_tokens(account_token_values: typing.List[OwnedTokenValue]) -> str:
|
def format_tokens(account_token_values: typing.Sequence[OwnedTokenValue]) -> str:
|
||||||
if len(account_token_values) == 0:
|
if len(account_token_values) == 0:
|
||||||
return "None"
|
return "None"
|
||||||
return "\n ".join([f"{atv}" for atv in account_token_values])
|
return "\n ".join([f"{atv}" for atv in account_token_values])
|
||||||
|
@ -396,7 +398,7 @@ class TransactionScout:
|
||||||
# # 🥭 fetch_all_recent_transaction_signatures function
|
# # 🥭 fetch_all_recent_transaction_signatures function
|
||||||
#
|
#
|
||||||
|
|
||||||
def fetch_all_recent_transaction_signatures(context: Context, in_the_last: datetime.timedelta = datetime.timedelta(days=1)) -> typing.List[str]:
|
def fetch_all_recent_transaction_signatures(context: Context, in_the_last: datetime.timedelta = datetime.timedelta(days=1)) -> typing.Sequence[str]:
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
recency_cutoff = now - in_the_last
|
recency_cutoff = now - in_the_last
|
||||||
recency_cutoff_timestamp = recency_cutoff.timestamp()
|
recency_cutoff_timestamp = recency_cutoff.timestamp()
|
||||||
|
|
|
@ -133,7 +133,7 @@ class PercentageTargetBalance(TargetBalance):
|
||||||
#
|
#
|
||||||
|
|
||||||
class TargetBalanceParser:
|
class TargetBalanceParser:
|
||||||
def __init__(self, tokens: typing.List[Token]):
|
def __init__(self, tokens: typing.Sequence[Token]):
|
||||||
self.tokens = tokens
|
self.tokens = tokens
|
||||||
|
|
||||||
def parse(self, to_parse: str) -> TargetBalance:
|
def parse(self, to_parse: str) -> TargetBalance:
|
||||||
|
@ -176,7 +176,7 @@ class TargetBalanceParser:
|
||||||
# into account for this sorting but we don't need to now so we don't.)
|
# into account for this sorting but we don't need to now so we don't.)
|
||||||
#
|
#
|
||||||
|
|
||||||
def sort_changes_for_trades(changes: typing.List[TokenValue]) -> typing.List[TokenValue]:
|
def sort_changes_for_trades(changes: typing.Sequence[TokenValue]) -> typing.Sequence[TokenValue]:
|
||||||
return sorted(changes, key=lambda change: change.value)
|
return sorted(changes, key=lambda change: change.value)
|
||||||
|
|
||||||
|
|
||||||
|
@ -186,7 +186,7 @@ def sort_changes_for_trades(changes: typing.List[TokenValue]) -> typing.List[Tok
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
def calculate_required_balance_changes(current_balances: typing.List[TokenValue], desired_balances: typing.List[TokenValue]) -> typing.List[TokenValue]:
|
def calculate_required_balance_changes(current_balances: typing.Sequence[TokenValue], desired_balances: typing.Sequence[TokenValue]) -> typing.Sequence[TokenValue]:
|
||||||
changes: typing.List[TokenValue] = []
|
changes: typing.List[TokenValue] = []
|
||||||
for desired in desired_balances:
|
for desired in desired_balances:
|
||||||
current = TokenValue.find_by_token(current_balances, desired.token)
|
current = TokenValue.find_by_token(current_balances, desired.token)
|
||||||
|
@ -211,7 +211,7 @@ def calculate_required_balance_changes(current_balances: typing.List[TokenValue]
|
||||||
|
|
||||||
|
|
||||||
class FilterSmallChanges:
|
class FilterSmallChanges:
|
||||||
def __init__(self, action_threshold: Decimal, balances: typing.List[TokenValue], prices: typing.List[TokenValue]):
|
def __init__(self, action_threshold: Decimal, balances: typing.Sequence[TokenValue], prices: typing.Sequence[TokenValue]):
|
||||||
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
||||||
self.prices: typing.Dict[str, TokenValue] = {}
|
self.prices: typing.Dict[str, TokenValue] = {}
|
||||||
total = Decimal(0)
|
total = Decimal(0)
|
||||||
|
@ -258,7 +258,7 @@ class FilterSmallChanges:
|
||||||
|
|
||||||
class WalletBalancer(metaclass=abc.ABCMeta):
|
class WalletBalancer(metaclass=abc.ABCMeta):
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def balance(self, prices: typing.List[TokenValue]):
|
def balance(self, prices: typing.Sequence[TokenValue]):
|
||||||
raise NotImplementedError("WalletBalancer.balance() is not implemented on the base type.")
|
raise NotImplementedError("WalletBalancer.balance() is not implemented on the base type.")
|
||||||
|
|
||||||
|
|
||||||
|
@ -270,7 +270,7 @@ class WalletBalancer(metaclass=abc.ABCMeta):
|
||||||
|
|
||||||
|
|
||||||
class NullWalletBalancer(WalletBalancer):
|
class NullWalletBalancer(WalletBalancer):
|
||||||
def balance(self, prices: typing.List[TokenValue]):
|
def balance(self, prices: typing.Sequence[TokenValue]):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -280,17 +280,17 @@ class NullWalletBalancer(WalletBalancer):
|
||||||
#
|
#
|
||||||
|
|
||||||
class LiveWalletBalancer(WalletBalancer):
|
class LiveWalletBalancer(WalletBalancer):
|
||||||
def __init__(self, context: Context, wallet: Wallet, group: Group, trade_executor: TradeExecutor, action_threshold: Decimal, tokens: typing.List[Token], target_balances: typing.List[TargetBalance]):
|
def __init__(self, context: Context, wallet: Wallet, group: Group, trade_executor: TradeExecutor, action_threshold: Decimal, tokens: typing.Sequence[Token], target_balances: typing.Sequence[TargetBalance]):
|
||||||
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
||||||
self.context: Context = context
|
self.context: Context = context
|
||||||
self.wallet: Wallet = wallet
|
self.wallet: Wallet = wallet
|
||||||
self.group: Group = group
|
self.group: Group = group
|
||||||
self.trade_executor: TradeExecutor = trade_executor
|
self.trade_executor: TradeExecutor = trade_executor
|
||||||
self.action_threshold: Decimal = action_threshold
|
self.action_threshold: Decimal = action_threshold
|
||||||
self.tokens: typing.List[Token] = tokens
|
self.tokens: typing.Sequence[Token] = tokens
|
||||||
self.target_balances: typing.List[TargetBalance] = target_balances
|
self.target_balances: typing.Sequence[TargetBalance] = target_balances
|
||||||
|
|
||||||
def balance(self, prices: typing.List[TokenValue]):
|
def balance(self, prices: typing.Sequence[TokenValue]):
|
||||||
padding = "\n "
|
padding = "\n "
|
||||||
|
|
||||||
def balances_report(balances) -> str:
|
def balances_report(balances) -> str:
|
||||||
|
@ -323,7 +323,7 @@ class LiveWalletBalancer(WalletBalancer):
|
||||||
updated_balances = self._fetch_balances()
|
updated_balances = self._fetch_balances()
|
||||||
self.logger.info(f"Finishing balances: {padding}{balances_report(updated_balances)}")
|
self.logger.info(f"Finishing balances: {padding}{balances_report(updated_balances)}")
|
||||||
|
|
||||||
def _make_changes(self, balance_changes: typing.List[TokenValue]):
|
def _make_changes(self, balance_changes: typing.Sequence[TokenValue]):
|
||||||
self.logger.info(f"Balance changes to make: {balance_changes}")
|
self.logger.info(f"Balance changes to make: {balance_changes}")
|
||||||
quote = self.group.shared_quote_token.token.symbol
|
quote = self.group.shared_quote_token.token.symbol
|
||||||
for change in balance_changes:
|
for change in balance_changes:
|
||||||
|
@ -333,7 +333,7 @@ class LiveWalletBalancer(WalletBalancer):
|
||||||
else:
|
else:
|
||||||
self.trade_executor.buy(market_symbol, change.value.copy_abs())
|
self.trade_executor.buy(market_symbol, change.value.copy_abs())
|
||||||
|
|
||||||
def _fetch_balances(self) -> typing.List[TokenValue]:
|
def _fetch_balances(self) -> typing.Sequence[TokenValue]:
|
||||||
balances: typing.List[TokenValue] = []
|
balances: typing.List[TokenValue] = []
|
||||||
for token in self.tokens:
|
for token in self.tokens:
|
||||||
balance = TokenValue.fetch_total_value(self.context, self.wallet.address, token)
|
balance = TokenValue.fetch_total_value(self.context, self.wallet.address, token)
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import datetime
|
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
from pyserum import market
|
from pyserum import market
|
||||||
|
@ -37,6 +35,11 @@ def fake_token() -> mango.Token:
|
||||||
return mango.Token("FAKE", "Fake Token", fake_seeded_public_key("fake token"), Decimal(6))
|
return mango.Token("FAKE", "Fake Token", fake_seeded_public_key("fake token"), Decimal(6))
|
||||||
|
|
||||||
|
|
||||||
|
def fake_token_info() -> mango.TokenInfo:
|
||||||
|
token = fake_token()
|
||||||
|
return mango.TokenInfo(token, fake_seeded_public_key("root bank"), Decimal(7))
|
||||||
|
|
||||||
|
|
||||||
def fake_context() -> mango.Context:
|
def fake_context() -> mango.Context:
|
||||||
context = mango.Context(cluster="test",
|
context = mango.Context(cluster="test",
|
||||||
cluster_url="http://localhost",
|
cluster_url="http://localhost",
|
||||||
|
@ -48,13 +51,6 @@ def fake_context() -> mango.Context:
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
def fake_index() -> mango.Index:
|
|
||||||
token = fake_token()
|
|
||||||
borrow = mango.TokenValue(token, Decimal(0))
|
|
||||||
deposit = mango.TokenValue(token, Decimal(0))
|
|
||||||
return mango.Index(mango.Version.V1, token, datetime.datetime.now(), borrow, deposit)
|
|
||||||
|
|
||||||
|
|
||||||
def fake_market() -> market.Market:
|
def fake_market() -> market.Market:
|
||||||
Container = NamedTuple("Container", [("own_address", PublicKey), ("vault_signer_nonce", int)])
|
Container = NamedTuple("Container", [("own_address", PublicKey), ("vault_signer_nonce", int)])
|
||||||
container = Container(own_address=fake_seeded_public_key("market address"), vault_signer_nonce=2)
|
container = Container(own_address=fake_seeded_public_key("market address"), vault_signer_nonce=2)
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -3,9 +3,8 @@ from .fakes import fake_account_info, fake_context, fake_seeded_public_key
|
||||||
|
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from solana.publickey import PublicKey
|
from mango.layouts import layouts
|
||||||
|
|
||||||
#
|
#
|
||||||
# Mocks are more involved than fakes, but do tend to allow more introspection.
|
# Mocks are more involved than fakes, but do tend to allow more introspection.
|
||||||
|
@ -22,52 +21,26 @@ USDC = token_lookup.find_by_symbol_or_raise("USDC")
|
||||||
def mock_group():
|
def mock_group():
|
||||||
account_info = fake_account_info()
|
account_info = fake_account_info()
|
||||||
name = "FAKE_GROUP"
|
name = "FAKE_GROUP"
|
||||||
account_flags = mango.MangoAccountFlags(mango.Version.V1, True, False, True, False)
|
meta_data = mango.Metadata(layouts.DATA_TYPE.Group, mango.Version.V1, True)
|
||||||
|
btc_info = mango.TokenInfo(BTC, fake_seeded_public_key("root bank"), Decimal(6))
|
||||||
def index(token):
|
usdc_info = mango.TokenInfo(USDC, fake_seeded_public_key("root bank"), Decimal(6))
|
||||||
borrow = mango.TokenValue(token, Decimal(1))
|
token_infos = [btc_info, None, usdc_info]
|
||||||
deposit = mango.TokenValue(token, Decimal(1))
|
spot_markets = []
|
||||||
return mango.Index(mango.Version.V1, token, datetime.now(), borrow, deposit)
|
perp_markets = []
|
||||||
|
oracles = []
|
||||||
basket_tokens = [
|
|
||||||
mango.BasketToken(ETH, fake_seeded_public_key("ETH vault"), index(ETH)),
|
|
||||||
mango.BasketToken(BTC, fake_seeded_public_key("BTC vault"), index(ETH)),
|
|
||||||
mango.BasketToken(SOL, fake_seeded_public_key("SOL vault"), index(ETH)),
|
|
||||||
mango.BasketToken(SRM, fake_seeded_public_key("SRM vault"), index(ETH)),
|
|
||||||
mango.BasketToken(USDC, fake_seeded_public_key("USDC vault"), index(ETH))
|
|
||||||
]
|
|
||||||
|
|
||||||
markets = []
|
|
||||||
signer_nonce = Decimal(1)
|
signer_nonce = Decimal(1)
|
||||||
signer_key = fake_seeded_public_key("signer key")
|
signer_key = fake_seeded_public_key("signer key")
|
||||||
|
admin_key = fake_seeded_public_key("admin key")
|
||||||
dex_program_id = fake_seeded_public_key("DEX program ID")
|
dex_program_id = fake_seeded_public_key("DEX program ID")
|
||||||
total_deposits = [
|
cache_key = fake_seeded_public_key("cache key")
|
||||||
mango.TokenValue(ETH, Decimal(1000)),
|
valid_interval = Decimal(7)
|
||||||
mango.TokenValue(BTC, Decimal(1000)),
|
|
||||||
mango.TokenValue(SOL, Decimal(1000)),
|
|
||||||
mango.TokenValue(SRM, Decimal(1000)),
|
|
||||||
mango.TokenValue(USDC, Decimal(1000)),
|
|
||||||
]
|
|
||||||
total_borrows = [
|
|
||||||
mango.TokenValue(ETH, Decimal(0)),
|
|
||||||
mango.TokenValue(BTC, Decimal(0)),
|
|
||||||
mango.TokenValue(SOL, Decimal(0)),
|
|
||||||
mango.TokenValue(SRM, Decimal(0)),
|
|
||||||
mango.TokenValue(USDC, Decimal(0)),
|
|
||||||
]
|
|
||||||
maint_coll_ratio = Decimal("1.1")
|
|
||||||
init_coll_ratio = Decimal("1.2")
|
|
||||||
srm_vault = fake_seeded_public_key("SRM vault")
|
|
||||||
admin = fake_seeded_public_key("admin")
|
|
||||||
borrow_limits = [Decimal(10), Decimal(10), Decimal(10), Decimal(10), Decimal(10)]
|
|
||||||
|
|
||||||
return mango.Group(account_info, mango.Version.V1, name, account_flags,
|
return mango.Group(account_info, mango.Version.V1, name, meta_data, token_infos,
|
||||||
basket_tokens, markets, signer_nonce, signer_key, dex_program_id,
|
spot_markets, perp_markets, oracles, signer_nonce, signer_key,
|
||||||
total_deposits, total_borrows, maint_coll_ratio, init_coll_ratio,
|
admin_key, dex_program_id, cache_key, valid_interval)
|
||||||
srm_vault, admin, borrow_limits)
|
|
||||||
|
|
||||||
|
|
||||||
def mock_prices(prices: typing.List[str]):
|
def mock_prices(prices: typing.Sequence[str]):
|
||||||
eth, btc, sol, srm, usdc = prices
|
eth, btc, sol, srm, usdc = prices
|
||||||
return [
|
return [
|
||||||
mango.TokenValue(ETH, Decimal(eth)),
|
mango.TokenValue(ETH, Decimal(eth)),
|
||||||
|
@ -78,48 +51,13 @@ def mock_prices(prices: typing.List[str]):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def mock_margin_account(group: mango.Group, deposits: typing.List[str], borrows: typing.List[str], openorders: typing.List[typing.Optional[mango.OpenOrders]]):
|
|
||||||
eth, btc, sol, srm, usdc = deposits
|
|
||||||
token_deposits = [
|
|
||||||
mango.TokenValue(ETH, Decimal(eth)),
|
|
||||||
mango.TokenValue(BTC, Decimal(btc)),
|
|
||||||
mango.TokenValue(SOL, Decimal(sol)),
|
|
||||||
mango.TokenValue(SRM, Decimal(srm)),
|
|
||||||
mango.TokenValue(USDC, Decimal(usdc)),
|
|
||||||
]
|
|
||||||
eth, btc, sol, srm, usdc = borrows
|
|
||||||
token_borrows = [
|
|
||||||
mango.TokenValue(ETH, Decimal(eth)),
|
|
||||||
mango.TokenValue(BTC, Decimal(btc)),
|
|
||||||
mango.TokenValue(SOL, Decimal(sol)),
|
|
||||||
mango.TokenValue(SRM, Decimal(srm)),
|
|
||||||
mango.TokenValue(USDC, Decimal(usdc)),
|
|
||||||
]
|
|
||||||
|
|
||||||
account_flags = mango.MangoAccountFlags(mango.Version.V1, True, False, True, False)
|
|
||||||
has_borrows = False
|
|
||||||
owner = fake_seeded_public_key("owner")
|
|
||||||
being_liquidated = False
|
|
||||||
open_orders_keys: typing.List[typing.Optional[PublicKey]] = []
|
|
||||||
for oo in openorders:
|
|
||||||
if oo is None:
|
|
||||||
open_orders_keys += [None]
|
|
||||||
else:
|
|
||||||
open_orders_keys += [oo.address]
|
|
||||||
margin_account = mango.MarginAccount(fake_account_info(), mango.Version.V1, account_flags,
|
|
||||||
has_borrows, group.address, owner, being_liquidated,
|
|
||||||
token_deposits, token_borrows, open_orders_keys)
|
|
||||||
margin_account.open_orders_accounts = openorders
|
|
||||||
return margin_account
|
|
||||||
|
|
||||||
|
|
||||||
def mock_open_orders(base_token_free: Decimal = Decimal(0), base_token_total: Decimal = Decimal(0), quote_token_free: Decimal = Decimal(0), quote_token_total: Decimal = Decimal(0), referrer_rebate_accrued: Decimal = Decimal(0)):
|
def mock_open_orders(base_token_free: Decimal = Decimal(0), base_token_total: Decimal = Decimal(0), quote_token_free: Decimal = Decimal(0), quote_token_total: Decimal = Decimal(0), referrer_rebate_accrued: Decimal = Decimal(0)):
|
||||||
account_info = fake_account_info()
|
account_info = fake_account_info()
|
||||||
program_id = fake_seeded_public_key("program ID")
|
program_id = fake_seeded_public_key("program ID")
|
||||||
market = fake_seeded_public_key("market")
|
market = fake_seeded_public_key("market")
|
||||||
owner = fake_seeded_public_key("owner")
|
owner = fake_seeded_public_key("owner")
|
||||||
|
|
||||||
flags = mango.SerumAccountFlags(mango.Version.V1, True, False, True, False, False, False, False, False)
|
flags = mango.AccountFlags(mango.Version.V1, True, False, True, False, False, False, False, False)
|
||||||
return mango.OpenOrders(account_info, mango.Version.V1, program_id, flags, market,
|
return mango.OpenOrders(account_info, mango.Version.V1, program_id, flags, market,
|
||||||
owner, base_token_free, base_token_total, quote_token_free,
|
owner, base_token_free, base_token_total, quote_token_free,
|
||||||
quote_token_total, Decimal(0), Decimal(0), [], [],
|
quote_token_total, Decimal(0), Decimal(0), [], [],
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
from .context import mango
|
||||||
|
from .fakes import fake_account_info, fake_seeded_public_key
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
from mango.layouts import layouts
|
||||||
|
|
||||||
|
|
||||||
|
def test_construction():
|
||||||
|
account_info = fake_account_info()
|
||||||
|
meta_data = mango.Metadata(layouts.DATA_TYPE.Group, mango.Version.V1, True)
|
||||||
|
group = fake_seeded_public_key("group")
|
||||||
|
owner = fake_seeded_public_key("owner")
|
||||||
|
in_basket = [Decimal(1), Decimal(0), Decimal(3)]
|
||||||
|
deposits = [Decimal(10), Decimal(0), Decimal(5)]
|
||||||
|
borrows = [Decimal(0), Decimal(0), Decimal(0)]
|
||||||
|
spot_open_orders = [fake_seeded_public_key("spot1"), fake_seeded_public_key(
|
||||||
|
"spot2"), fake_seeded_public_key("spot3")]
|
||||||
|
|
||||||
|
# TODO - this isn't right.
|
||||||
|
perp_accounts = [fake_seeded_public_key("perp1"), fake_seeded_public_key("perp2"), fake_seeded_public_key("perp3")]
|
||||||
|
|
||||||
|
actual = mango.Account(account_info, mango.Version.V1, meta_data, group, owner,
|
||||||
|
in_basket, deposits, borrows, spot_open_orders, perp_accounts)
|
||||||
|
|
||||||
|
assert actual is not None
|
||||||
|
assert actual.logger is not None
|
||||||
|
assert actual.version == mango.Version.V1
|
||||||
|
assert actual.meta_data == meta_data
|
||||||
|
assert actual.group == group
|
||||||
|
assert actual.owner == owner
|
||||||
|
assert actual.in_basket == in_basket
|
||||||
|
assert actual.deposits == deposits
|
||||||
|
assert actual.borrows == borrows
|
||||||
|
assert actual.spot_open_orders == spot_open_orders
|
||||||
|
assert actual.perp_accounts == perp_accounts
|
|
@ -10,8 +10,8 @@ def test_constructor():
|
||||||
bids: bool = True
|
bids: bool = True
|
||||||
asks: bool = True
|
asks: bool = True
|
||||||
disabled: bool = True
|
disabled: bool = True
|
||||||
actual = mango.SerumAccountFlags(mango.Version.V1, initialized, market, open_orders,
|
actual = mango.AccountFlags(mango.Version.V1, initialized, market, open_orders,
|
||||||
request_queue, event_queue, bids, asks, disabled)
|
request_queue, event_queue, bids, asks, disabled)
|
||||||
assert actual is not None
|
assert actual is not None
|
||||||
assert actual.logger is not None
|
assert actual.logger is not None
|
||||||
assert actual.version == mango.Version.V1
|
assert actual.version == mango.Version.V1
|
||||||
|
@ -24,9 +24,9 @@ def test_constructor():
|
||||||
assert actual.asks == asks
|
assert actual.asks == asks
|
||||||
assert actual.disabled == disabled
|
assert actual.disabled == disabled
|
||||||
|
|
||||||
actual2 = mango.SerumAccountFlags(mango.Version.V2, not initialized, not market,
|
actual2 = mango.AccountFlags(mango.Version.V2, not initialized, not market,
|
||||||
not open_orders, not request_queue, not event_queue,
|
not open_orders, not request_queue, not event_queue,
|
||||||
not bids, not asks, not disabled)
|
not bids, not asks, not disabled)
|
||||||
assert actual2 is not None
|
assert actual2 is not None
|
||||||
assert actual2.logger is not None
|
assert actual2.logger is not None
|
||||||
assert actual2.version == mango.Version.V2
|
assert actual2.version == mango.Version.V2
|
|
@ -1,5 +1,4 @@
|
||||||
from .context import mango
|
from .context import mango
|
||||||
from .fakes import fake_context
|
|
||||||
|
|
||||||
|
|
||||||
def test_account_liquidator_constructor():
|
def test_account_liquidator_constructor():
|
||||||
|
@ -16,39 +15,3 @@ def test_null_account_liquidator_constructor():
|
||||||
actual = mango.NullAccountLiquidator()
|
actual = mango.NullAccountLiquidator()
|
||||||
assert actual is not None
|
assert actual is not None
|
||||||
assert actual.logger is not None
|
assert actual.logger is not None
|
||||||
|
|
||||||
|
|
||||||
def test_actual_account_liquidator_constructor():
|
|
||||||
context: mango.Context = fake_context()
|
|
||||||
wallet: mango.Wallet = {"fake": "Walle"}
|
|
||||||
actual = mango.ActualAccountLiquidator(context, wallet)
|
|
||||||
assert actual is not None
|
|
||||||
assert actual.logger is not None
|
|
||||||
assert actual.context == context
|
|
||||||
assert actual.wallet == wallet
|
|
||||||
|
|
||||||
|
|
||||||
def test_force_cancel_orders_account_liquidator_constructor():
|
|
||||||
context: mango.Context = fake_context()
|
|
||||||
wallet: mango.Wallet = {"fake": "Walle"}
|
|
||||||
actual = mango.ForceCancelOrdersAccountLiquidator(context, wallet)
|
|
||||||
assert actual is not None
|
|
||||||
assert actual.logger is not None
|
|
||||||
assert actual.context == context
|
|
||||||
assert actual.wallet == wallet
|
|
||||||
|
|
||||||
|
|
||||||
def test_reporting_account_liquidator_constructor():
|
|
||||||
inner = mango.NullAccountLiquidator()
|
|
||||||
context: mango.Context = fake_context()
|
|
||||||
wallet: mango.Wallet = {"fake": "Walle"}
|
|
||||||
liquidations_publisher: mango.EventSource[mango.LiquidationEvent] = mango.EventSource()
|
|
||||||
liquidator_name = "Test"
|
|
||||||
actual = mango.ReportingAccountLiquidator(inner, context, wallet, liquidations_publisher, liquidator_name)
|
|
||||||
assert actual is not None
|
|
||||||
assert actual.logger is not None
|
|
||||||
assert actual.inner == inner
|
|
||||||
assert actual.context == context
|
|
||||||
assert actual.wallet == wallet
|
|
||||||
assert actual.liquidations_publisher == liquidations_publisher
|
|
||||||
assert actual.liquidator_name == liquidator_name
|
|
||||||
|
|
|
@ -1,102 +0,0 @@
|
||||||
from .context import mango
|
|
||||||
from .fakes import fake_account_info, fake_seeded_public_key
|
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from decimal import Decimal
|
|
||||||
from solana.publickey import PublicKey
|
|
||||||
|
|
||||||
|
|
||||||
def test_aggregator_config_constructor():
|
|
||||||
description: str = "Test Aggregator Config"
|
|
||||||
decimals: Decimal = Decimal(5)
|
|
||||||
restart_delay: Decimal = Decimal(30)
|
|
||||||
max_submissions: Decimal = Decimal(10)
|
|
||||||
min_submissions: Decimal = Decimal(2)
|
|
||||||
reward_amount: Decimal = Decimal(30)
|
|
||||||
reward_token_account: PublicKey = fake_seeded_public_key("reward token account")
|
|
||||||
actual = mango.AggregatorConfig(mango.Version.V1, description, decimals, restart_delay,
|
|
||||||
max_submissions, min_submissions, reward_amount, reward_token_account)
|
|
||||||
assert actual is not None
|
|
||||||
assert actual.logger is not None
|
|
||||||
assert actual.version == mango.Version.V1
|
|
||||||
assert actual.description == description
|
|
||||||
assert actual.decimals == decimals
|
|
||||||
assert actual.restart_delay == restart_delay
|
|
||||||
assert actual.max_submissions == max_submissions
|
|
||||||
assert actual.min_submissions == min_submissions
|
|
||||||
assert actual.reward_amount == reward_amount
|
|
||||||
assert actual.reward_token_account == reward_token_account
|
|
||||||
|
|
||||||
|
|
||||||
def test_round_constructor():
|
|
||||||
id: Decimal = Decimal(85)
|
|
||||||
updated_at: datetime = datetime.now()
|
|
||||||
created_at: datetime = updated_at - timedelta(minutes=5)
|
|
||||||
actual = mango.Round(mango.Version.V1, id, created_at, updated_at)
|
|
||||||
assert actual is not None
|
|
||||||
assert actual.logger is not None
|
|
||||||
assert actual.version == mango.Version.V1
|
|
||||||
assert actual.id == id
|
|
||||||
assert actual.created_at == created_at
|
|
||||||
assert actual.updated_at == updated_at
|
|
||||||
|
|
||||||
# def __init__(self, version: Version, round_id: Decimal, median: Decimal, created_at: datetime.datetime, updated_at: datetime.datetime):
|
|
||||||
|
|
||||||
|
|
||||||
def test_answer_constructor():
|
|
||||||
round_id: Decimal = Decimal(85)
|
|
||||||
median: Decimal = Decimal(25)
|
|
||||||
updated_at: datetime = datetime.now()
|
|
||||||
created_at: datetime = updated_at - timedelta(minutes=5)
|
|
||||||
actual = mango.Answer(mango.Version.V1, round_id, median, created_at, updated_at)
|
|
||||||
assert actual is not None
|
|
||||||
assert actual.logger is not None
|
|
||||||
assert actual.version == mango.Version.V1
|
|
||||||
assert actual.round_id == round_id
|
|
||||||
assert actual.median == median
|
|
||||||
assert actual.created_at == created_at
|
|
||||||
assert actual.updated_at == updated_at
|
|
||||||
|
|
||||||
|
|
||||||
def test_aggregator_constructor():
|
|
||||||
account_info = fake_account_info()
|
|
||||||
description: str = "Test Aggregator Config"
|
|
||||||
decimals: Decimal = Decimal(5)
|
|
||||||
restart_delay: Decimal = Decimal(30)
|
|
||||||
max_submissions: Decimal = Decimal(10)
|
|
||||||
min_submissions: Decimal = Decimal(2)
|
|
||||||
reward_amount: Decimal = Decimal(30)
|
|
||||||
reward_token_account: PublicKey = fake_seeded_public_key("reward token account")
|
|
||||||
config = mango.AggregatorConfig(mango.Version.V1, description, decimals, restart_delay,
|
|
||||||
max_submissions, min_submissions, reward_amount, reward_token_account)
|
|
||||||
initialized = True
|
|
||||||
name = "Test Aggregator"
|
|
||||||
owner = fake_seeded_public_key("owner")
|
|
||||||
|
|
||||||
id: Decimal = Decimal(85)
|
|
||||||
updated_at: datetime = datetime.now()
|
|
||||||
created_at: datetime = updated_at - timedelta(minutes=5)
|
|
||||||
round = mango.Round(mango.Version.V1, id, created_at, updated_at)
|
|
||||||
|
|
||||||
round_submissions = fake_seeded_public_key("round submissions")
|
|
||||||
|
|
||||||
round_id: Decimal = Decimal(85)
|
|
||||||
median: Decimal = Decimal(25)
|
|
||||||
answer = mango.Answer(mango.Version.V1, round_id, median, created_at, updated_at)
|
|
||||||
|
|
||||||
answer_submissions = fake_seeded_public_key("answer submissions")
|
|
||||||
|
|
||||||
actual = mango.Aggregator(account_info, mango.Version.V1, config, initialized,
|
|
||||||
name, owner, round, round_submissions, answer, answer_submissions)
|
|
||||||
assert actual is not None
|
|
||||||
assert actual.logger is not None
|
|
||||||
assert actual.account_info == account_info
|
|
||||||
assert actual.version == mango.Version.V1
|
|
||||||
assert actual.config == config
|
|
||||||
assert actual.initialized == initialized
|
|
||||||
assert actual.name == name
|
|
||||||
assert actual.owner == owner
|
|
||||||
assert actual.round == round
|
|
||||||
assert actual.round_submissions == round_submissions
|
|
||||||
assert actual.answer == answer
|
|
||||||
assert actual.answer_submissions == answer_submissions
|
|
|
@ -1,14 +0,0 @@
|
||||||
from .context import mango
|
|
||||||
from .fakes import fake_index, fake_seeded_public_key, fake_token
|
|
||||||
|
|
||||||
|
|
||||||
def test_constructor():
|
|
||||||
token = fake_token()
|
|
||||||
vault = fake_seeded_public_key("vault")
|
|
||||||
index = fake_index()
|
|
||||||
actual = mango.BasketToken(token, vault, index)
|
|
||||||
assert actual is not None
|
|
||||||
assert actual.logger is not None
|
|
||||||
assert actual.token == token
|
|
||||||
assert actual.vault == vault
|
|
||||||
assert actual.index == index
|
|
File diff suppressed because one or more lines are too long
|
@ -1,19 +0,0 @@
|
||||||
from .context import mango
|
|
||||||
from .fakes import fake_token
|
|
||||||
|
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
|
|
||||||
def test_constructor():
|
|
||||||
last_update = datetime.datetime.now()
|
|
||||||
token = fake_token()
|
|
||||||
borrow = mango.TokenValue(token, Decimal(27))
|
|
||||||
deposit = mango.TokenValue(token, Decimal(62))
|
|
||||||
actual = mango.Index(mango.Version.V1, token, last_update, borrow, deposit)
|
|
||||||
assert actual is not None
|
|
||||||
assert actual.logger is not None
|
|
||||||
assert actual.last_update == last_update
|
|
||||||
assert actual.borrow == borrow
|
|
||||||
assert actual.deposit == deposit
|
|
|
@ -1,7 +1,7 @@
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from .context import mango
|
from .context import mango
|
||||||
from .fakes import fake_account_info, fake_context, fake_index, fake_market, fake_seeded_public_key, fake_token
|
from .fakes import fake_context, fake_market, fake_seeded_public_key, fake_token
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from pyserum.enums import OrderType, Side
|
from pyserum.enums import OrderType, Side
|
||||||
|
@ -20,64 +20,6 @@ def test_instruction_builder_constructor():
|
||||||
assert succeeded
|
assert succeeded
|
||||||
|
|
||||||
|
|
||||||
def test_force_cancel_orders_instruction_builder_constructor():
|
|
||||||
context: mango.Context = fake_context()
|
|
||||||
group: mango.Group = {"fake": "Group"}
|
|
||||||
wallet: mango.Wallet = {"fake": "Wallet"}
|
|
||||||
margin_account: mango.MarginAccount = {"fake": "MarginAccount"}
|
|
||||||
market_metadata: mango.MarketMetadata = {"fake": "MarketMetadata"}
|
|
||||||
market: Market = {"fake": "Market"}
|
|
||||||
oracles: mango.typing.List[PublicKey] = [fake_seeded_public_key("oracle")]
|
|
||||||
dex_signer: mango.PublicKey = fake_seeded_public_key("DEX signer")
|
|
||||||
actual = mango.ForceCancelOrdersInstructionBuilder(context, group, wallet, margin_account,
|
|
||||||
market_metadata, market, oracles,
|
|
||||||
dex_signer)
|
|
||||||
assert actual is not None
|
|
||||||
assert actual.logger is not None
|
|
||||||
assert actual.context == context
|
|
||||||
assert actual.group == group
|
|
||||||
assert actual.wallet == wallet
|
|
||||||
assert actual.margin_account == margin_account
|
|
||||||
assert actual.market_metadata == market_metadata
|
|
||||||
assert actual.market == market
|
|
||||||
assert actual.oracles == oracles
|
|
||||||
assert actual.dex_signer == dex_signer
|
|
||||||
|
|
||||||
|
|
||||||
def test_liquidate_instruction_builder_constructor():
|
|
||||||
context: mango.Context = fake_context()
|
|
||||||
group: mango.Group = {"fake": "Group"}
|
|
||||||
wallet: mango.Wallet = {"fake": "Wallet"}
|
|
||||||
margin_account: mango.MarginAccount = {"fake": "MarginAccount"}
|
|
||||||
oracles: mango.typing.List[PublicKey] = [fake_seeded_public_key("oracle")]
|
|
||||||
input_token = mango.BasketToken(fake_token(), fake_seeded_public_key("vault"), fake_index())
|
|
||||||
input_token_value = mango.TokenValue(input_token.token, Decimal(30))
|
|
||||||
output_token = mango.BasketToken(fake_token(), fake_seeded_public_key("vault"), fake_index())
|
|
||||||
output_token_value = mango.TokenValue(output_token.token, Decimal(40))
|
|
||||||
wallet_input_token_account = mango.TokenAccount(
|
|
||||||
fake_account_info(), mango.Version.V1, fake_seeded_public_key("owner"), input_token_value)
|
|
||||||
wallet_output_token_account = mango.TokenAccount(
|
|
||||||
fake_account_info(), mango.Version.V1, fake_seeded_public_key("owner"), output_token_value)
|
|
||||||
maximum_input_amount = Decimal(50)
|
|
||||||
actual = mango.LiquidateInstructionBuilder(context, group, wallet, margin_account,
|
|
||||||
oracles, input_token, output_token,
|
|
||||||
wallet_input_token_account,
|
|
||||||
wallet_output_token_account,
|
|
||||||
maximum_input_amount)
|
|
||||||
assert actual is not None
|
|
||||||
assert actual.logger is not None
|
|
||||||
assert actual.context == context
|
|
||||||
assert actual.group == group
|
|
||||||
assert actual.wallet == wallet
|
|
||||||
assert actual.margin_account == margin_account
|
|
||||||
assert actual.oracles == oracles
|
|
||||||
assert actual.input_token == input_token
|
|
||||||
assert actual.output_token == output_token
|
|
||||||
assert actual.wallet_input_token_account == wallet_input_token_account
|
|
||||||
assert actual.wallet_output_token_account == wallet_output_token_account
|
|
||||||
assert actual.maximum_input_amount == maximum_input_amount
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_spl_account_instruction_builder_constructor():
|
def test_create_spl_account_instruction_builder_constructor():
|
||||||
context: mango.Context = fake_context()
|
context: mango.Context = fake_context()
|
||||||
wallet: mango.Wallet = {"fake": "Wallet"}
|
wallet: mango.Wallet = {"fake": "Wallet"}
|
||||||
|
@ -181,7 +123,7 @@ def test_consume_events_instruction_builder_constructor():
|
||||||
context: mango.Context = fake_context()
|
context: mango.Context = fake_context()
|
||||||
wallet: mango.Wallet = {"fake": "Wallet"}
|
wallet: mango.Wallet = {"fake": "Wallet"}
|
||||||
market: Market = {"fake": "Market"}
|
market: Market = {"fake": "Market"}
|
||||||
open_orders_addresses: typing.List[PublicKey] = [fake_seeded_public_key("open orders account")]
|
open_orders_addresses: typing.Sequence[PublicKey] = [fake_seeded_public_key("open orders account")]
|
||||||
limit: int = 64
|
limit: int = 64
|
||||||
actual = mango.ConsumeEventsInstructionBuilder(context, wallet, market, open_orders_addresses, limit)
|
actual = mango.ConsumeEventsInstructionBuilder(context, wallet, market, open_orders_addresses, limit)
|
||||||
assert actual is not None
|
assert actual is not None
|
||||||
|
|
|
@ -1,406 +1,406 @@
|
||||||
from .context import mango
|
# from .context import mango
|
||||||
from .mocks import mock_group, mock_prices, mock_margin_account, mock_open_orders
|
# from .mocks import mock_group, mock_prices, mock_margin_account, mock_open_orders
|
||||||
|
|
||||||
import typing
|
# import typing
|
||||||
|
|
||||||
from decimal import Decimal
|
# from decimal import Decimal
|
||||||
|
|
||||||
|
|
||||||
def test_constructor():
|
# def test_constructor():
|
||||||
group = mock_group()
|
# group = mock_group()
|
||||||
prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
||||||
deposits = ["0", "0", "0", "0", "1000"]
|
# deposits = ["0", "0", "0", "0", "1000"]
|
||||||
borrows = ["0", "0", "0", "0", "0"]
|
# borrows = ["0", "0", "0", "0", "0"]
|
||||||
margin_account = mock_margin_account(group, deposits, borrows, [])
|
# margin_account = mock_margin_account(group, deposits, borrows, [])
|
||||||
balance_sheet = margin_account.get_balance_sheet_totals(group, prices)
|
# balance_sheet = margin_account.get_balance_sheet_totals(group, prices)
|
||||||
balances = margin_account.get_intrinsic_balances(group)
|
# balances = margin_account.get_intrinsic_balances(group)
|
||||||
state = mango.LiquidatableState.RIPE | mango.LiquidatableState.LIQUIDATABLE | mango.LiquidatableState.ABOVE_WATER
|
# state = mango.LiquidatableState.RIPE | mango.LiquidatableState.LIQUIDATABLE | mango.LiquidatableState.ABOVE_WATER
|
||||||
worthwhile_threshold = Decimal(1)
|
# worthwhile_threshold = Decimal(1)
|
||||||
actual = mango.LiquidatableReport(group, prices, margin_account, balance_sheet,
|
# actual = mango.LiquidatableReport(group, prices, margin_account, balance_sheet,
|
||||||
balances, state, worthwhile_threshold)
|
# balances, state, worthwhile_threshold)
|
||||||
|
|
||||||
assert actual is not None
|
# assert actual is not None
|
||||||
assert actual.logger is not None
|
# assert actual.logger is not None
|
||||||
assert actual.balance_sheet == balance_sheet
|
# assert actual.balance_sheet == balance_sheet
|
||||||
assert actual.balances == balances
|
# assert actual.balances == balances
|
||||||
assert actual.state != mango.LiquidatableState.UNSET
|
# assert actual.state != mango.LiquidatableState.UNSET
|
||||||
assert actual.state & mango.LiquidatableState.RIPE
|
# assert actual.state & mango.LiquidatableState.RIPE
|
||||||
assert actual.state & mango.LiquidatableState.LIQUIDATABLE
|
# assert actual.state & mango.LiquidatableState.LIQUIDATABLE
|
||||||
assert actual.state & mango.LiquidatableState.ABOVE_WATER
|
# assert actual.state & mango.LiquidatableState.ABOVE_WATER
|
||||||
assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
|
# assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
|
||||||
assert actual.worthwhile_threshold == worthwhile_threshold
|
# assert actual.worthwhile_threshold == worthwhile_threshold
|
||||||
|
|
||||||
|
|
||||||
def test_non_ripe_account_no_openorders():
|
# def test_non_ripe_account_no_openorders():
|
||||||
group = mock_group()
|
# group = mock_group()
|
||||||
prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
||||||
deposits = ["0", "0", "0", "0", "1000"]
|
# deposits = ["0", "0", "0", "0", "1000"]
|
||||||
borrows = ["0", "0", "0", "0", "0"]
|
# borrows = ["0", "0", "0", "0", "0"]
|
||||||
margin_account = mock_margin_account(group, deposits, borrows, [])
|
# margin_account = mock_margin_account(group, deposits, borrows, [])
|
||||||
worthwhile_threshold = Decimal("0.01")
|
# worthwhile_threshold = Decimal("0.01")
|
||||||
|
|
||||||
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
||||||
assert actual.state == mango.LiquidatableState.UNSET
|
# assert actual.state == mango.LiquidatableState.UNSET
|
||||||
assert not (actual.state & mango.LiquidatableState.RIPE)
|
# assert not (actual.state & mango.LiquidatableState.RIPE)
|
||||||
assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE)
|
# assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE)
|
||||||
assert not (actual.state & mango.LiquidatableState.ABOVE_WATER)
|
# assert not (actual.state & mango.LiquidatableState.ABOVE_WATER)
|
||||||
assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
|
# assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
|
||||||
|
|
||||||
|
|
||||||
def test_ripe_account_no_openorders():
|
# def test_ripe_account_no_openorders():
|
||||||
group = mock_group()
|
# group = mock_group()
|
||||||
prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
||||||
deposits = ["0", "0", "0", "0", "1000"]
|
# deposits = ["0", "0", "0", "0", "1000"]
|
||||||
|
|
||||||
# 170 @ 5 = 850 for a collateral ratio of 1000/850 = 117% - ripe but not liquidatable
|
# # 170 @ 5 = 850 for a collateral ratio of 1000/850 = 117% - ripe but not liquidatable
|
||||||
borrows = ["0", "0", "0", "170", "0"]
|
# borrows = ["0", "0", "0", "170", "0"]
|
||||||
|
|
||||||
margin_account = mock_margin_account(group, deposits, borrows, [])
|
# margin_account = mock_margin_account(group, deposits, borrows, [])
|
||||||
worthwhile_threshold = Decimal("0.01")
|
# worthwhile_threshold = Decimal("0.01")
|
||||||
|
|
||||||
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
||||||
assert actual.state != mango.LiquidatableState.UNSET
|
# assert actual.state != mango.LiquidatableState.UNSET
|
||||||
assert actual.state & mango.LiquidatableState.RIPE
|
# assert actual.state & mango.LiquidatableState.RIPE
|
||||||
assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE)
|
# assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE)
|
||||||
assert actual.state & mango.LiquidatableState.ABOVE_WATER
|
# assert actual.state & mango.LiquidatableState.ABOVE_WATER
|
||||||
assert actual.state & mango.LiquidatableState.WORTHWHILE
|
# assert actual.state & mango.LiquidatableState.WORTHWHILE
|
||||||
|
|
||||||
|
|
||||||
def test_liquidatable_account_no_openorders():
|
# def test_liquidatable_account_no_openorders():
|
||||||
group = mock_group()
|
# group = mock_group()
|
||||||
prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
||||||
deposits = ["0", "0", "0", "0", "1000"]
|
# deposits = ["0", "0", "0", "0", "1000"]
|
||||||
|
|
||||||
# 200 @ 5 = 1000 for a collateral ratio of 1000/1000 = 100% - liquidatable but not above water
|
# # 200 @ 5 = 1000 for a collateral ratio of 1000/1000 = 100% - liquidatable but not above water
|
||||||
borrows = ["0", "0", "0", "200", "0"]
|
# borrows = ["0", "0", "0", "200", "0"]
|
||||||
|
|
||||||
margin_account = mock_margin_account(group, deposits, borrows, [])
|
# margin_account = mock_margin_account(group, deposits, borrows, [])
|
||||||
worthwhile_threshold = Decimal("0.01")
|
# worthwhile_threshold = Decimal("0.01")
|
||||||
|
|
||||||
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
||||||
assert actual.state != mango.LiquidatableState.UNSET
|
# assert actual.state != mango.LiquidatableState.UNSET
|
||||||
assert actual.state & mango.LiquidatableState.RIPE
|
# assert actual.state & mango.LiquidatableState.RIPE
|
||||||
assert actual.state & mango.LiquidatableState.LIQUIDATABLE
|
# assert actual.state & mango.LiquidatableState.LIQUIDATABLE
|
||||||
assert not (actual.state & mango.LiquidatableState.ABOVE_WATER)
|
# assert not (actual.state & mango.LiquidatableState.ABOVE_WATER)
|
||||||
assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
|
# assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
|
||||||
|
|
||||||
|
|
||||||
def test_above_water_account_no_openorders():
|
# def test_above_water_account_no_openorders():
|
||||||
group = mock_group()
|
# group = mock_group()
|
||||||
prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
||||||
deposits = ["0", "0", "0", "0", "1000"]
|
# deposits = ["0", "0", "0", "0", "1000"]
|
||||||
|
|
||||||
# 199.998 @ 5 = 999.99 for a collateral ratio of 1000/999.99 = 100.001% - liquidatable and above
|
# # 199.998 @ 5 = 999.99 for a collateral ratio of 1000/999.99 = 100.001% - liquidatable and above
|
||||||
# water but not worthwhile because the $0.01 available is not greater than the whorthwhile_threshold.
|
# # water but not worthwhile because the $0.01 available is not greater than the whorthwhile_threshold.
|
||||||
borrows = ["0", "0", "0", "199.998", "0"]
|
# borrows = ["0", "0", "0", "199.998", "0"]
|
||||||
|
|
||||||
margin_account = mock_margin_account(group, deposits, borrows, [])
|
# margin_account = mock_margin_account(group, deposits, borrows, [])
|
||||||
worthwhile_threshold = Decimal("0.01")
|
# worthwhile_threshold = Decimal("0.01")
|
||||||
|
|
||||||
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
||||||
assert actual.state != mango.LiquidatableState.UNSET
|
# assert actual.state != mango.LiquidatableState.UNSET
|
||||||
assert actual.state & mango.LiquidatableState.RIPE
|
# assert actual.state & mango.LiquidatableState.RIPE
|
||||||
assert actual.state & mango.LiquidatableState.LIQUIDATABLE
|
# assert actual.state & mango.LiquidatableState.LIQUIDATABLE
|
||||||
assert actual.state & mango.LiquidatableState.ABOVE_WATER
|
# assert actual.state & mango.LiquidatableState.ABOVE_WATER
|
||||||
assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
|
# assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
|
||||||
|
|
||||||
|
|
||||||
def test_worthwhile_account_no_openorders():
|
# def test_worthwhile_account_no_openorders():
|
||||||
group = mock_group()
|
# group = mock_group()
|
||||||
prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
||||||
deposits = ["0", "0", "0", "0", "1000"]
|
# deposits = ["0", "0", "0", "0", "1000"]
|
||||||
|
|
||||||
# 199.99 @ 5 = 999.95 for a collateral ratio of 1000/999.99 = 100.005% - liquidatable, above water
|
# # 199.99 @ 5 = 999.95 for a collateral ratio of 1000/999.99 = 100.005% - liquidatable, above water
|
||||||
# and worthwhile because the $0.05 available is greater than the whorthwhile_threshold.
|
# # and worthwhile because the $0.05 available is greater than the whorthwhile_threshold.
|
||||||
borrows = ["0", "0", "0", "199.99", "0"]
|
# borrows = ["0", "0", "0", "199.99", "0"]
|
||||||
|
|
||||||
margin_account = mock_margin_account(group, deposits, borrows, [])
|
# margin_account = mock_margin_account(group, deposits, borrows, [])
|
||||||
worthwhile_threshold = Decimal("0.01")
|
# worthwhile_threshold = Decimal("0.01")
|
||||||
|
|
||||||
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
||||||
assert actual.state != mango.LiquidatableState.UNSET
|
# assert actual.state != mango.LiquidatableState.UNSET
|
||||||
assert actual.state & mango.LiquidatableState.RIPE
|
# assert actual.state & mango.LiquidatableState.RIPE
|
||||||
assert actual.state & mango.LiquidatableState.LIQUIDATABLE
|
# assert actual.state & mango.LiquidatableState.LIQUIDATABLE
|
||||||
assert actual.state & mango.LiquidatableState.ABOVE_WATER
|
# assert actual.state & mango.LiquidatableState.ABOVE_WATER
|
||||||
assert actual.state & mango.LiquidatableState.WORTHWHILE
|
# assert actual.state & mango.LiquidatableState.WORTHWHILE
|
||||||
|
|
||||||
|
|
||||||
def test_non_ripe_account_openorders_base():
|
# def test_non_ripe_account_openorders_base():
|
||||||
group = mock_group()
|
# group = mock_group()
|
||||||
prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
||||||
deposits = ["0", "0", "0", "0", "1000"]
|
# deposits = ["0", "0", "0", "0", "1000"]
|
||||||
borrows = ["0", "0", "0", "100", "0"]
|
# borrows = ["0", "0", "0", "100", "0"]
|
||||||
open_orders: typing.List[mango.OpenOrders] = [None,
|
# open_orders: typing.Sequence[mango.OpenOrders] = [None,
|
||||||
None,
|
# None,
|
||||||
None,
|
# None,
|
||||||
mock_open_orders(base_token_total=Decimal(0)),
|
# mock_open_orders(base_token_total=Decimal(0)),
|
||||||
None]
|
# None]
|
||||||
margin_account = mock_margin_account(group, deposits, borrows, open_orders)
|
# margin_account = mock_margin_account(group, deposits, borrows, open_orders)
|
||||||
worthwhile_threshold = Decimal("0.01")
|
# worthwhile_threshold = Decimal("0.01")
|
||||||
|
|
||||||
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
||||||
assert actual.state != mango.LiquidatableState.UNSET
|
# assert actual.state != mango.LiquidatableState.UNSET
|
||||||
assert not (actual.state & mango.LiquidatableState.RIPE)
|
# assert not (actual.state & mango.LiquidatableState.RIPE)
|
||||||
assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE)
|
# assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE)
|
||||||
assert actual.state & mango.LiquidatableState.ABOVE_WATER
|
# assert actual.state & mango.LiquidatableState.ABOVE_WATER
|
||||||
assert actual.state & mango.LiquidatableState.WORTHWHILE
|
# assert actual.state & mango.LiquidatableState.WORTHWHILE
|
||||||
|
|
||||||
|
|
||||||
# The idea of these OpenOrders tests is that the deposits and borrows remain the same and the only
|
# # The idea of these OpenOrders tests is that the deposits and borrows remain the same and the only
|
||||||
# differences are through changes in OpenOrders balances.
|
# # differences are through changes in OpenOrders balances.
|
||||||
|
|
||||||
def test_ripe_account_openorders_base():
|
# def test_ripe_account_openorders_base():
|
||||||
group = mock_group()
|
# group = mock_group()
|
||||||
prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
||||||
deposits = ["0", "0", "0", "0", "900"]
|
# deposits = ["0", "0", "0", "0", "900"]
|
||||||
borrows = ["0", "0", "0", "200", "0"]
|
# borrows = ["0", "0", "0", "200", "0"]
|
||||||
|
|
||||||
# 1150/1000 = 115% - ripe but not liquidatable
|
# # 1150/1000 = 115% - ripe but not liquidatable
|
||||||
open_orders: typing.List[mango.OpenOrders] = [None,
|
# open_orders: typing.Sequence[mango.OpenOrders] = [None,
|
||||||
None,
|
# None,
|
||||||
None,
|
# None,
|
||||||
mock_open_orders(base_token_total=Decimal(50)),
|
# mock_open_orders(base_token_total=Decimal(50)),
|
||||||
None
|
# None
|
||||||
]
|
# ]
|
||||||
margin_account = mock_margin_account(group, deposits, borrows, open_orders)
|
# margin_account = mock_margin_account(group, deposits, borrows, open_orders)
|
||||||
worthwhile_threshold = Decimal("0.01")
|
# worthwhile_threshold = Decimal("0.01")
|
||||||
|
|
||||||
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
||||||
assert actual.state != mango.LiquidatableState.UNSET
|
# assert actual.state != mango.LiquidatableState.UNSET
|
||||||
assert actual.state & mango.LiquidatableState.RIPE
|
# assert actual.state & mango.LiquidatableState.RIPE
|
||||||
assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE)
|
# assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE)
|
||||||
assert actual.state & mango.LiquidatableState.ABOVE_WATER
|
# assert actual.state & mango.LiquidatableState.ABOVE_WATER
|
||||||
assert actual.state & mango.LiquidatableState.WORTHWHILE
|
# assert actual.state & mango.LiquidatableState.WORTHWHILE
|
||||||
|
|
||||||
|
|
||||||
def test_liquidatable_account_openorders_base():
|
# def test_liquidatable_account_openorders_base():
|
||||||
group = mock_group()
|
# group = mock_group()
|
||||||
prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
||||||
deposits = ["0", "0", "0", "0", "900"]
|
# deposits = ["0", "0", "0", "0", "900"]
|
||||||
borrows = ["0", "0", "0", "200", "0"]
|
# borrows = ["0", "0", "0", "200", "0"]
|
||||||
|
|
||||||
# 200 @ 5 = 1000 for a collateral ratio of 1000/1000 = 100% - liquidatable but not above water
|
# # 200 @ 5 = 1000 for a collateral ratio of 1000/1000 = 100% - liquidatable but not above water
|
||||||
open_orders: typing.List[mango.OpenOrders] = [None,
|
# open_orders: typing.Sequence[mango.OpenOrders] = [None,
|
||||||
None,
|
# None,
|
||||||
None,
|
# None,
|
||||||
mock_open_orders(base_token_total=Decimal(20)),
|
# mock_open_orders(base_token_total=Decimal(20)),
|
||||||
None]
|
# None]
|
||||||
margin_account = mock_margin_account(group, deposits, borrows, open_orders)
|
# margin_account = mock_margin_account(group, deposits, borrows, open_orders)
|
||||||
worthwhile_threshold = Decimal("0.01")
|
# worthwhile_threshold = Decimal("0.01")
|
||||||
|
|
||||||
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
||||||
assert actual.state != mango.LiquidatableState.UNSET
|
# assert actual.state != mango.LiquidatableState.UNSET
|
||||||
assert actual.state & mango.LiquidatableState.RIPE
|
# assert actual.state & mango.LiquidatableState.RIPE
|
||||||
assert actual.state & mango.LiquidatableState.LIQUIDATABLE
|
# assert actual.state & mango.LiquidatableState.LIQUIDATABLE
|
||||||
assert not (actual.state & mango.LiquidatableState.ABOVE_WATER)
|
# assert not (actual.state & mango.LiquidatableState.ABOVE_WATER)
|
||||||
assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
|
# assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
|
||||||
|
|
||||||
|
|
||||||
def test_above_water_account_openorders_base():
|
# def test_above_water_account_openorders_base():
|
||||||
group = mock_group()
|
# group = mock_group()
|
||||||
prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
||||||
deposits = ["0", "0", "0", "0", "900"]
|
# deposits = ["0", "0", "0", "0", "900"]
|
||||||
borrows = ["0", "0", "0", "200", "0"]
|
# borrows = ["0", "0", "0", "200", "0"]
|
||||||
|
|
||||||
# 900 + (20.002 @ 5) = 1000.01 for a collateral ratio of 1000.01/1000 = 100.001% - liquidatable and above
|
# # 900 + (20.002 @ 5) = 1000.01 for a collateral ratio of 1000.01/1000 = 100.001% - liquidatable and above
|
||||||
# water but not worthwhile because the $0.01 available is not greater than the whorthwhile_threshold.
|
# # water but not worthwhile because the $0.01 available is not greater than the whorthwhile_threshold.
|
||||||
open_orders: typing.List[mango.OpenOrders] = [None,
|
# open_orders: typing.Sequence[mango.OpenOrders] = [None,
|
||||||
None,
|
# None,
|
||||||
None,
|
# None,
|
||||||
mock_open_orders(base_token_total=Decimal("20.002")),
|
# mock_open_orders(base_token_total=Decimal("20.002")),
|
||||||
None]
|
# None]
|
||||||
margin_account = mock_margin_account(group, deposits, borrows, open_orders)
|
# margin_account = mock_margin_account(group, deposits, borrows, open_orders)
|
||||||
worthwhile_threshold = Decimal("0.01")
|
# worthwhile_threshold = Decimal("0.01")
|
||||||
|
|
||||||
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
||||||
assert actual.state != mango.LiquidatableState.UNSET
|
# assert actual.state != mango.LiquidatableState.UNSET
|
||||||
assert actual.state & mango.LiquidatableState.RIPE
|
# assert actual.state & mango.LiquidatableState.RIPE
|
||||||
assert actual.state & mango.LiquidatableState.LIQUIDATABLE
|
# assert actual.state & mango.LiquidatableState.LIQUIDATABLE
|
||||||
assert actual.state & mango.LiquidatableState.ABOVE_WATER
|
# assert actual.state & mango.LiquidatableState.ABOVE_WATER
|
||||||
assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
|
# assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
|
||||||
|
|
||||||
|
|
||||||
def test_worthwhile_account_openorders_base():
|
# def test_worthwhile_account_openorders_base():
|
||||||
group = mock_group()
|
# group = mock_group()
|
||||||
prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
||||||
deposits = ["0", "0", "0", "0", "900"]
|
# deposits = ["0", "0", "0", "0", "900"]
|
||||||
borrows = ["0", "0", "0", "200", "0"]
|
# borrows = ["0", "0", "0", "200", "0"]
|
||||||
|
|
||||||
# 900 + (20.003 @ 5) = 1000.015 for a collateral ratio of 1000.015/1000 = 100.0015% - liquidatable, above water
|
# # 900 + (20.003 @ 5) = 1000.015 for a collateral ratio of 1000.015/1000 = 100.0015% - liquidatable, above water
|
||||||
# and worthwhile because the $0.015 available is greater than the whorthwhile_threshold.
|
# # and worthwhile because the $0.015 available is greater than the whorthwhile_threshold.
|
||||||
open_orders: typing.List[mango.OpenOrders] = [None,
|
# open_orders: typing.Sequence[mango.OpenOrders] = [None,
|
||||||
None,
|
# None,
|
||||||
None,
|
# None,
|
||||||
mock_open_orders(base_token_total=Decimal("20.003")),
|
# mock_open_orders(base_token_total=Decimal("20.003")),
|
||||||
None]
|
# None]
|
||||||
|
|
||||||
margin_account = mock_margin_account(group, deposits, borrows, open_orders)
|
# margin_account = mock_margin_account(group, deposits, borrows, open_orders)
|
||||||
worthwhile_threshold = Decimal("0.01")
|
# worthwhile_threshold = Decimal("0.01")
|
||||||
|
|
||||||
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
||||||
assert actual.state != mango.LiquidatableState.UNSET
|
# assert actual.state != mango.LiquidatableState.UNSET
|
||||||
assert actual.state & mango.LiquidatableState.RIPE
|
# assert actual.state & mango.LiquidatableState.RIPE
|
||||||
assert actual.state & mango.LiquidatableState.LIQUIDATABLE
|
# assert actual.state & mango.LiquidatableState.LIQUIDATABLE
|
||||||
assert actual.state & mango.LiquidatableState.ABOVE_WATER
|
# assert actual.state & mango.LiquidatableState.ABOVE_WATER
|
||||||
assert actual.state & mango.LiquidatableState.WORTHWHILE
|
# assert actual.state & mango.LiquidatableState.WORTHWHILE
|
||||||
|
|
||||||
|
|
||||||
def test_non_ripe_account_openorders_quote():
|
# def test_non_ripe_account_openorders_quote():
|
||||||
group = mock_group()
|
# group = mock_group()
|
||||||
prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
||||||
deposits = ["0", "0", "0", "0", "1000"]
|
# deposits = ["0", "0", "0", "0", "1000"]
|
||||||
borrows = ["0", "0", "0", "100", "0"]
|
# borrows = ["0", "0", "0", "100", "0"]
|
||||||
open_orders: typing.List[mango.OpenOrders] = [None,
|
# open_orders: typing.Sequence[mango.OpenOrders] = [None,
|
||||||
None,
|
# None,
|
||||||
None,
|
# None,
|
||||||
mock_open_orders(quote_token_total=Decimal(0)),
|
# mock_open_orders(quote_token_total=Decimal(0)),
|
||||||
None]
|
# None]
|
||||||
margin_account = mock_margin_account(group, deposits, borrows, open_orders)
|
# margin_account = mock_margin_account(group, deposits, borrows, open_orders)
|
||||||
worthwhile_threshold = Decimal("0.01")
|
# worthwhile_threshold = Decimal("0.01")
|
||||||
|
|
||||||
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
||||||
assert actual.state != mango.LiquidatableState.UNSET
|
# assert actual.state != mango.LiquidatableState.UNSET
|
||||||
assert not (actual.state & mango.LiquidatableState.RIPE)
|
# assert not (actual.state & mango.LiquidatableState.RIPE)
|
||||||
assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE)
|
# assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE)
|
||||||
assert actual.state & mango.LiquidatableState.ABOVE_WATER
|
# assert actual.state & mango.LiquidatableState.ABOVE_WATER
|
||||||
assert actual.state & mango.LiquidatableState.WORTHWHILE
|
# assert actual.state & mango.LiquidatableState.WORTHWHILE
|
||||||
|
|
||||||
|
|
||||||
# The idea of these OpenOrders tests is that the deposits and borrows remain the same and the only
|
# # The idea of these OpenOrders tests is that the deposits and borrows remain the same and the only
|
||||||
# differences are through changes in OpenOrders balances.
|
# # differences are through changes in OpenOrders balances.
|
||||||
|
|
||||||
def test_ripe_account_openorders_quote():
|
# def test_ripe_account_openorders_quote():
|
||||||
group = mock_group()
|
# group = mock_group()
|
||||||
prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
||||||
deposits = ["0", "0", "0", "0", "900"]
|
# deposits = ["0", "0", "0", "0", "900"]
|
||||||
borrows = ["0", "0", "0", "200", "0"]
|
# borrows = ["0", "0", "0", "200", "0"]
|
||||||
|
|
||||||
# 1150/1000 = 115% - ripe but not liquidatable
|
# # 1150/1000 = 115% - ripe but not liquidatable
|
||||||
open_orders: typing.List[mango.OpenOrders] = [None,
|
# open_orders: typing.Sequence[mango.OpenOrders] = [None,
|
||||||
None,
|
# None,
|
||||||
None,
|
# None,
|
||||||
mock_open_orders(quote_token_total=Decimal(250)),
|
# mock_open_orders(quote_token_total=Decimal(250)),
|
||||||
None
|
# None
|
||||||
]
|
# ]
|
||||||
margin_account = mock_margin_account(group, deposits, borrows, open_orders)
|
# margin_account = mock_margin_account(group, deposits, borrows, open_orders)
|
||||||
worthwhile_threshold = Decimal("0.01")
|
# worthwhile_threshold = Decimal("0.01")
|
||||||
|
|
||||||
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
||||||
assert actual.state != mango.LiquidatableState.UNSET
|
# assert actual.state != mango.LiquidatableState.UNSET
|
||||||
assert actual.state & mango.LiquidatableState.RIPE
|
# assert actual.state & mango.LiquidatableState.RIPE
|
||||||
assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE)
|
# assert not (actual.state & mango.LiquidatableState.LIQUIDATABLE)
|
||||||
assert actual.state & mango.LiquidatableState.ABOVE_WATER
|
# assert actual.state & mango.LiquidatableState.ABOVE_WATER
|
||||||
assert actual.state & mango.LiquidatableState.WORTHWHILE
|
# assert actual.state & mango.LiquidatableState.WORTHWHILE
|
||||||
|
|
||||||
|
|
||||||
def test_liquidatable_account_openorders_quote():
|
# def test_liquidatable_account_openorders_quote():
|
||||||
group = mock_group()
|
# group = mock_group()
|
||||||
prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
||||||
deposits = ["0", "0", "0", "0", "900"]
|
# deposits = ["0", "0", "0", "0", "900"]
|
||||||
borrows = ["0", "0", "0", "200", "0"]
|
# borrows = ["0", "0", "0", "200", "0"]
|
||||||
|
|
||||||
# 200 @ 5 = 1000 for a collateral ratio of 1000/1000 = 100% - liquidatable but not above water
|
# # 200 @ 5 = 1000 for a collateral ratio of 1000/1000 = 100% - liquidatable but not above water
|
||||||
open_orders: typing.List[mango.OpenOrders] = [None,
|
# open_orders: typing.Sequence[mango.OpenOrders] = [None,
|
||||||
None,
|
# None,
|
||||||
None,
|
# None,
|
||||||
mock_open_orders(quote_token_total=Decimal(100)),
|
# mock_open_orders(quote_token_total=Decimal(100)),
|
||||||
None]
|
# None]
|
||||||
margin_account = mock_margin_account(group, deposits, borrows, open_orders)
|
# margin_account = mock_margin_account(group, deposits, borrows, open_orders)
|
||||||
worthwhile_threshold = Decimal("0.01")
|
# worthwhile_threshold = Decimal("0.01")
|
||||||
|
|
||||||
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
||||||
assert actual.state != mango.LiquidatableState.UNSET
|
# assert actual.state != mango.LiquidatableState.UNSET
|
||||||
assert actual.state & mango.LiquidatableState.RIPE
|
# assert actual.state & mango.LiquidatableState.RIPE
|
||||||
assert actual.state & mango.LiquidatableState.LIQUIDATABLE
|
# assert actual.state & mango.LiquidatableState.LIQUIDATABLE
|
||||||
assert not (actual.state & mango.LiquidatableState.ABOVE_WATER)
|
# assert not (actual.state & mango.LiquidatableState.ABOVE_WATER)
|
||||||
assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
|
# assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
|
||||||
|
|
||||||
|
|
||||||
def test_above_water_account_openorders_quote():
|
# def test_above_water_account_openorders_quote():
|
||||||
group = mock_group()
|
# group = mock_group()
|
||||||
prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
||||||
deposits = ["0", "0", "0", "0", "900"]
|
# deposits = ["0", "0", "0", "0", "900"]
|
||||||
borrows = ["0", "0", "0", "200", "0"]
|
# borrows = ["0", "0", "0", "200", "0"]
|
||||||
|
|
||||||
# 900 + 100.01 for a collateral ratio of 1000.01/1000 = 100.001% - liquidatable and above
|
# # 900 + 100.01 for a collateral ratio of 1000.01/1000 = 100.001% - liquidatable and above
|
||||||
# water but not worthwhile because the $0.01 available is not greater than the whorthwhile_threshold.
|
# # water but not worthwhile because the $0.01 available is not greater than the whorthwhile_threshold.
|
||||||
open_orders: typing.List[mango.OpenOrders] = [None,
|
# open_orders: typing.Sequence[mango.OpenOrders] = [None,
|
||||||
None,
|
# None,
|
||||||
None,
|
# None,
|
||||||
mock_open_orders(quote_token_total=Decimal("100.01")),
|
# mock_open_orders(quote_token_total=Decimal("100.01")),
|
||||||
None]
|
# None]
|
||||||
margin_account = mock_margin_account(group, deposits, borrows, open_orders)
|
# margin_account = mock_margin_account(group, deposits, borrows, open_orders)
|
||||||
worthwhile_threshold = Decimal("0.01")
|
# worthwhile_threshold = Decimal("0.01")
|
||||||
|
|
||||||
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
||||||
assert actual.state != mango.LiquidatableState.UNSET
|
# assert actual.state != mango.LiquidatableState.UNSET
|
||||||
assert actual.state & mango.LiquidatableState.RIPE
|
# assert actual.state & mango.LiquidatableState.RIPE
|
||||||
assert actual.state & mango.LiquidatableState.LIQUIDATABLE
|
# assert actual.state & mango.LiquidatableState.LIQUIDATABLE
|
||||||
assert actual.state & mango.LiquidatableState.ABOVE_WATER
|
# assert actual.state & mango.LiquidatableState.ABOVE_WATER
|
||||||
assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
|
# assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
|
||||||
|
|
||||||
|
|
||||||
def test_worthwhile_account_openorders_quote():
|
# def test_worthwhile_account_openorders_quote():
|
||||||
group = mock_group()
|
# group = mock_group()
|
||||||
prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
||||||
deposits = ["0", "0", "0", "0", "900"]
|
# deposits = ["0", "0", "0", "0", "900"]
|
||||||
borrows = ["0", "0", "0", "200", "0"]
|
# borrows = ["0", "0", "0", "200", "0"]
|
||||||
|
|
||||||
# 199.99 @ 5 = 999.95 for a collateral ratio of 1000/999.99 = 100.005% - liquidatable, above water
|
# # 199.99 @ 5 = 999.95 for a collateral ratio of 1000/999.99 = 100.005% - liquidatable, above water
|
||||||
# and worthwhile because the $0.05 available is greater than the whorthwhile_threshold.
|
# # and worthwhile because the $0.05 available is greater than the whorthwhile_threshold.
|
||||||
open_orders: typing.List[mango.OpenOrders] = [None,
|
# open_orders: typing.Sequence[mango.OpenOrders] = [None,
|
||||||
None,
|
# None,
|
||||||
None,
|
# None,
|
||||||
mock_open_orders(quote_token_total=Decimal(101)),
|
# mock_open_orders(quote_token_total=Decimal(101)),
|
||||||
None]
|
# None]
|
||||||
|
|
||||||
margin_account = mock_margin_account(group, deposits, borrows, open_orders)
|
# margin_account = mock_margin_account(group, deposits, borrows, open_orders)
|
||||||
worthwhile_threshold = Decimal("0.01")
|
# worthwhile_threshold = Decimal("0.01")
|
||||||
|
|
||||||
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
||||||
assert actual.state != mango.LiquidatableState.UNSET
|
# assert actual.state != mango.LiquidatableState.UNSET
|
||||||
assert actual.state & mango.LiquidatableState.RIPE
|
# assert actual.state & mango.LiquidatableState.RIPE
|
||||||
assert actual.state & mango.LiquidatableState.LIQUIDATABLE
|
# assert actual.state & mango.LiquidatableState.LIQUIDATABLE
|
||||||
assert actual.state & mango.LiquidatableState.ABOVE_WATER
|
# assert actual.state & mango.LiquidatableState.ABOVE_WATER
|
||||||
assert actual.state & mango.LiquidatableState.WORTHWHILE
|
# assert actual.state & mango.LiquidatableState.WORTHWHILE
|
||||||
|
|
||||||
|
|
||||||
def test_liquidatable_account_referrer_fee():
|
# def test_liquidatable_account_referrer_fee():
|
||||||
group = mock_group()
|
# group = mock_group()
|
||||||
prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
# prices = mock_prices(["2000", "30000", "40", "5", "1"])
|
||||||
deposits = ["0", "0", "0", "0", "900"]
|
# deposits = ["0", "0", "0", "0", "900"]
|
||||||
borrows = ["0", "0", "0", "200", "0"]
|
# borrows = ["0", "0", "0", "200", "0"]
|
||||||
|
|
||||||
# 200 @ 5 = 1000 for a collateral ratio of 1000/1000 = 100% - liquidatable but not above water
|
# # 200 @ 5 = 1000 for a collateral ratio of 1000/1000 = 100% - liquidatable but not above water
|
||||||
open_orders: typing.List[mango.OpenOrders] = [None,
|
# open_orders: typing.Sequence[mango.OpenOrders] = [None,
|
||||||
None,
|
# None,
|
||||||
None,
|
# None,
|
||||||
mock_open_orders(quote_token_total=Decimal(100)),
|
# mock_open_orders(quote_token_total=Decimal(100)),
|
||||||
None]
|
# None]
|
||||||
margin_account = mock_margin_account(group, deposits, borrows, open_orders)
|
# margin_account = mock_margin_account(group, deposits, borrows, open_orders)
|
||||||
worthwhile_threshold = Decimal("0.01")
|
# worthwhile_threshold = Decimal("0.01")
|
||||||
|
|
||||||
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
||||||
assert actual.state != mango.LiquidatableState.UNSET
|
# assert actual.state != mango.LiquidatableState.UNSET
|
||||||
assert actual.state & mango.LiquidatableState.RIPE
|
# assert actual.state & mango.LiquidatableState.RIPE
|
||||||
assert actual.state & mango.LiquidatableState.LIQUIDATABLE
|
# assert actual.state & mango.LiquidatableState.LIQUIDATABLE
|
||||||
assert not (actual.state & mango.LiquidatableState.ABOVE_WATER)
|
# assert not (actual.state & mango.LiquidatableState.ABOVE_WATER)
|
||||||
assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
|
# assert not (actual.state & mango.LiquidatableState.WORTHWHILE)
|
||||||
|
|
||||||
# Exactly the same scenario as above, but with referrer_rebate_accrued=Decimal(1) in one OpenOrders.
|
# # Exactly the same scenario as above, but with referrer_rebate_accrued=Decimal(1) in one OpenOrders.
|
||||||
# This gives a collateral ratio of 1.001, which is ripe, liquidatable, above water and worthwhile.
|
# # This gives a collateral ratio of 1.001, which is ripe, liquidatable, above water and worthwhile.
|
||||||
open_orders: typing.List[mango.OpenOrders] = [None,
|
# open_orders: typing.Sequence[mango.OpenOrders] = [None,
|
||||||
None,
|
# None,
|
||||||
None,
|
# None,
|
||||||
mock_open_orders(quote_token_total=Decimal(100),
|
# mock_open_orders(quote_token_total=Decimal(100),
|
||||||
referrer_rebate_accrued=Decimal(1)),
|
# referrer_rebate_accrued=Decimal(1)),
|
||||||
None]
|
# None]
|
||||||
margin_account = mock_margin_account(group, deposits, borrows, open_orders)
|
# margin_account = mock_margin_account(group, deposits, borrows, open_orders)
|
||||||
worthwhile_threshold = Decimal("0.01")
|
# worthwhile_threshold = Decimal("0.01")
|
||||||
|
|
||||||
actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
# actual = mango.LiquidatableReport.build(group, prices, margin_account, worthwhile_threshold)
|
||||||
assert actual.state != mango.LiquidatableState.UNSET
|
# assert actual.state != mango.LiquidatableState.UNSET
|
||||||
assert actual.state & mango.LiquidatableState.RIPE
|
# assert actual.state & mango.LiquidatableState.RIPE
|
||||||
assert actual.state & mango.LiquidatableState.LIQUIDATABLE
|
# assert actual.state & mango.LiquidatableState.LIQUIDATABLE
|
||||||
assert actual.state & mango.LiquidatableState.ABOVE_WATER
|
# assert actual.state & mango.LiquidatableState.ABOVE_WATER
|
||||||
assert actual.state & mango.LiquidatableState.WORTHWHILE
|
# assert actual.state & mango.LiquidatableState.WORTHWHILE
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
from .context import mango
|
from .context import mango
|
||||||
from .fakes import fake_context
|
from .fakes import fake_context
|
||||||
from .mocks import mock_group, mock_prices, mock_margin_account, mock_open_orders
|
# from .mocks import mock_group, mock_prices, mock_open_orders
|
||||||
|
|
||||||
import typing
|
# import typing
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
@ -33,250 +33,250 @@ def test_constructor():
|
||||||
# additional code is common and shared across test functions.
|
# additional code is common and shared across test functions.
|
||||||
#
|
#
|
||||||
|
|
||||||
class LiquidateMock:
|
# class LiquidateMock:
|
||||||
def __init__(self, liquidation_processor: mango.LiquidationProcessor):
|
# def __init__(self, liquidation_processor: mango.LiquidationProcessor):
|
||||||
self.liquidation_processor = liquidation_processor
|
# self.liquidation_processor = liquidation_processor
|
||||||
self.captured_group: typing.Optional[mango.Group] = None
|
# self.captured_group: typing.Optional[mango.Group] = None
|
||||||
self.captured_prices: typing.Optional[typing.List[mango.TokenValue]] = None
|
# self.captured_prices: typing.Optional[typing.Sequence[mango.TokenValue]] = None
|
||||||
self.captured_to_liquidate: typing.Optional[typing.List[mango.LiquidatableReport]] = None
|
# self.captured_to_liquidate: typing.Optional[typing.Sequence[mango.LiquidatableReport]] = None
|
||||||
|
|
||||||
# This monkeypatch is a bit nasty. It would be better to make the LiquidationProcessor
|
# # This monkeypatch is a bit nasty. It would be better to make the LiquidationProcessor
|
||||||
# a bit more test-friendly.
|
# # a bit more test-friendly.
|
||||||
liquidation_processor._liquidate_all = self.liquidate_capture # type: ignore
|
# liquidation_processor._liquidate_all = self.liquidate_capture # type: ignore
|
||||||
|
|
||||||
def liquidate_capture(self, group: mango.Group, prices: typing.List[mango.TokenValue], to_liquidate: typing.List[mango.LiquidatableReport]):
|
# def liquidate_capture(self, group: mango.Group, prices: typing.Sequence[mango.TokenValue], to_liquidate: typing.Sequence[mango.LiquidatableReport]):
|
||||||
self.captured_group = group
|
# self.captured_group = group
|
||||||
self.captured_prices = prices
|
# self.captured_prices = prices
|
||||||
self.captured_to_liquidate = to_liquidate
|
# self.captured_to_liquidate = to_liquidate
|
||||||
|
|
||||||
|
|
||||||
def capturing_liquidation_processor() -> LiquidateMock:
|
# def capturing_liquidation_processor() -> LiquidateMock:
|
||||||
context: mango.Context = fake_context()
|
# context: mango.Context = fake_context()
|
||||||
name: str = "Test Liquidator"
|
# name: str = "Test Liquidator"
|
||||||
account_liquidator: mango.AccountLiquidator = mango.NullAccountLiquidator()
|
# account_liquidator: mango.AccountLiquidator = mango.NullAccountLiquidator()
|
||||||
wallet_balancer: mango.WalletBalancer = mango.NullWalletBalancer()
|
# wallet_balancer: mango.WalletBalancer = mango.NullWalletBalancer()
|
||||||
worthwhile_threshold: Decimal = Decimal("0.1")
|
# worthwhile_threshold: Decimal = Decimal("0.1")
|
||||||
actual = mango.LiquidationProcessor(context, name, account_liquidator, wallet_balancer, worthwhile_threshold)
|
# actual = mango.LiquidationProcessor(context, name, account_liquidator, wallet_balancer, worthwhile_threshold)
|
||||||
|
|
||||||
return LiquidateMock(actual)
|
# return LiquidateMock(actual)
|
||||||
|
|
||||||
|
|
||||||
def validate_liquidation_results(deposits: typing.List[str], borrows: typing.List[str], openorders: typing.List[typing.Optional[mango.OpenOrders]], price_iterations: typing.List[typing.Tuple[typing.List[str], str, bool]]):
|
# def validate_liquidation_results(deposits: typing.Sequence[str], borrows: typing.Sequence[str], openorders: typing.Sequence[typing.Optional[mango.OpenOrders]], price_iterations: typing.Sequence[typing.Tuple[typing.Sequence[str], str, bool]]):
|
||||||
group = mock_group()
|
# group = mock_group()
|
||||||
capturer = capturing_liquidation_processor()
|
# capturer = capturing_liquidation_processor()
|
||||||
margin_account = mock_margin_account(group,
|
# margin_account = mock_margin_account(group,
|
||||||
deposits,
|
# deposits,
|
||||||
borrows,
|
# borrows,
|
||||||
openorders)
|
# openorders)
|
||||||
for (prices, calculated_balance, liquidatable) in price_iterations:
|
# for (prices, calculated_balance, liquidatable) in price_iterations:
|
||||||
token_prices = mock_prices(prices)
|
# token_prices = mock_prices(prices)
|
||||||
balance_sheet = margin_account.get_balance_sheet_totals(group, token_prices)
|
# balance_sheet = margin_account.get_balance_sheet_totals(group, token_prices)
|
||||||
assert balance_sheet.assets - balance_sheet.liabilities == Decimal(calculated_balance)
|
# assert balance_sheet.assets - balance_sheet.liabilities == Decimal(calculated_balance)
|
||||||
|
|
||||||
capturer.liquidation_processor.update_margin_accounts([margin_account])
|
# capturer.liquidation_processor.update_margin_accounts([margin_account])
|
||||||
capturer.liquidation_processor.update_prices(group, token_prices)
|
# capturer.liquidation_processor.update_prices(group, token_prices)
|
||||||
|
|
||||||
if liquidatable:
|
# if liquidatable:
|
||||||
assert (capturer.captured_to_liquidate is not None) and (len(capturer.captured_to_liquidate) == 1)
|
# assert (capturer.captured_to_liquidate is not None) and (len(capturer.captured_to_liquidate) == 1)
|
||||||
assert capturer.captured_to_liquidate[0].margin_account == margin_account
|
# assert capturer.captured_to_liquidate[0].margin_account == margin_account
|
||||||
else:
|
# else:
|
||||||
assert (capturer.captured_to_liquidate is not None) and (len(capturer.captured_to_liquidate) == 0)
|
# assert (capturer.captured_to_liquidate is not None) and (len(capturer.captured_to_liquidate) == 0)
|
||||||
|
|
||||||
|
|
||||||
def test_non_liquidatable_account():
|
# def test_non_liquidatable_account():
|
||||||
# A simple case - no borrows
|
# # A simple case - no borrows
|
||||||
validate_liquidation_results(
|
# validate_liquidation_results(
|
||||||
["0", "0", "0", "0", "1000"],
|
# ["0", "0", "0", "0", "1000"],
|
||||||
["0", "0", "0", "0", "0"],
|
# ["0", "0", "0", "0", "0"],
|
||||||
[None, None, None, None, None],
|
# [None, None, None, None, None],
|
||||||
[
|
# [
|
||||||
(
|
# (
|
||||||
["2000", "30000", "40", "5", "1"],
|
# ["2000", "30000", "40", "5", "1"],
|
||||||
"1000",
|
# "1000",
|
||||||
False
|
# False
|
||||||
)
|
# )
|
||||||
]
|
# ]
|
||||||
)
|
# )
|
||||||
|
|
||||||
|
|
||||||
def test_liquidatable_account():
|
# def test_liquidatable_account():
|
||||||
# A simple case with no currency conversions - 1000 USDC and (somehow) borrowing
|
# # A simple case with no currency conversions - 1000 USDC and (somehow) borrowing
|
||||||
# 950 USDC
|
# # 950 USDC
|
||||||
validate_liquidation_results(
|
# validate_liquidation_results(
|
||||||
["0", "0", "0", "0", "1000"],
|
# ["0", "0", "0", "0", "1000"],
|
||||||
["0", "0", "0", "0", "950"],
|
# ["0", "0", "0", "0", "950"],
|
||||||
[None, None, None, None, None],
|
# [None, None, None, None, None],
|
||||||
[
|
# [
|
||||||
(
|
# (
|
||||||
["2000", "30000", "40", "5", "1"],
|
# ["2000", "30000", "40", "5", "1"],
|
||||||
"50",
|
# "50",
|
||||||
True
|
# True
|
||||||
)
|
# )
|
||||||
]
|
# ]
|
||||||
)
|
# )
|
||||||
|
|
||||||
|
|
||||||
def test_converted_balance_not_liquidatable():
|
# def test_converted_balance_not_liquidatable():
|
||||||
# A more realistic case. 1000 USDC, borrowed 180 SRM now @ 5 USDC
|
# # A more realistic case. 1000 USDC, borrowed 180 SRM now @ 5 USDC
|
||||||
validate_liquidation_results(
|
# validate_liquidation_results(
|
||||||
["0", "0", "0", "0", "1000"],
|
# ["0", "0", "0", "0", "1000"],
|
||||||
["0", "0", "0", "180", "0"],
|
# ["0", "0", "0", "180", "0"],
|
||||||
[None, None, None, None, None],
|
# [None, None, None, None, None],
|
||||||
[
|
# [
|
||||||
(
|
# (
|
||||||
["2000", "30000", "40", "5", "1"],
|
# ["2000", "30000", "40", "5", "1"],
|
||||||
"100",
|
# "100",
|
||||||
False
|
# False
|
||||||
)
|
# )
|
||||||
]
|
# ]
|
||||||
)
|
# )
|
||||||
|
|
||||||
|
|
||||||
def test_converted_balance_liquidatable():
|
# def test_converted_balance_liquidatable():
|
||||||
# 1000 USDC, borrowed 190 SRM now @ 5 USDC
|
# # 1000 USDC, borrowed 190 SRM now @ 5 USDC
|
||||||
validate_liquidation_results(
|
# validate_liquidation_results(
|
||||||
["0", "0", "0", "0", "1000"],
|
# ["0", "0", "0", "0", "1000"],
|
||||||
["0", "0", "0", "190", "0"],
|
# ["0", "0", "0", "190", "0"],
|
||||||
[None, None, None, None, None],
|
# [None, None, None, None, None],
|
||||||
[
|
# [
|
||||||
(
|
# (
|
||||||
["2000", "30000", "40", "5", "1"],
|
# ["2000", "30000", "40", "5", "1"],
|
||||||
"50",
|
# "50",
|
||||||
True
|
# True
|
||||||
)
|
# )
|
||||||
]
|
# ]
|
||||||
)
|
# )
|
||||||
|
|
||||||
|
|
||||||
def test_converted_balance_not_liquidatable_becomes_liquidatable_on_price_change():
|
# def test_converted_balance_not_liquidatable_becomes_liquidatable_on_price_change():
|
||||||
# 1000 USDC, borrowed 180 SRM @ 5 USDC, price goes to 5.2 USDC
|
# # 1000 USDC, borrowed 180 SRM @ 5 USDC, price goes to 5.2 USDC
|
||||||
validate_liquidation_results(
|
# validate_liquidation_results(
|
||||||
["0", "0", "0", "0", "1000"],
|
# ["0", "0", "0", "0", "1000"],
|
||||||
["0", "0", "0", "180", "0"],
|
# ["0", "0", "0", "180", "0"],
|
||||||
[None, None, None, None, None],
|
# [None, None, None, None, None],
|
||||||
[
|
# [
|
||||||
(
|
# (
|
||||||
["2000", "30000", "40", "5", "1"],
|
# ["2000", "30000", "40", "5", "1"],
|
||||||
"100",
|
# "100",
|
||||||
False
|
# False
|
||||||
),
|
# ),
|
||||||
(
|
# (
|
||||||
["2000", "30000", "40", "5.2", "1"],
|
# ["2000", "30000", "40", "5.2", "1"],
|
||||||
"64",
|
# "64",
|
||||||
True
|
# True
|
||||||
)
|
# )
|
||||||
]
|
# ]
|
||||||
)
|
# )
|
||||||
|
|
||||||
|
|
||||||
def test_converted_balance_liquidatable_becomes_not_liquidatable_on_price_change():
|
# def test_converted_balance_liquidatable_becomes_not_liquidatable_on_price_change():
|
||||||
# 1000 USDC, borrowed 180 SRM, price goes to 5.2 USDC (and account becomes liquidatable),
|
# # 1000 USDC, borrowed 180 SRM, price goes to 5.2 USDC (and account becomes liquidatable),
|
||||||
# then SRM price falls to 5 USDC (and account becomes non-liqudatable). Can margin
|
# # then SRM price falls to 5 USDC (and account becomes non-liqudatable). Can margin
|
||||||
# accounts switch from liquidatable (but not liquidated) to non-liquidatable? Yes - if
|
# # accounts switch from liquidatable (but not liquidated) to non-liquidatable? Yes - if
|
||||||
# something causes an error on the liquidation attempt, it's skipped until the next
|
# # something causes an error on the liquidation attempt, it's skipped until the next
|
||||||
# round (with fresh prices). If no-one else tries to liquidate it (unlikely), it'll
|
# # round (with fresh prices). If no-one else tries to liquidate it (unlikely), it'll
|
||||||
# appear in the next round as non-liquidatable.
|
# # appear in the next round as non-liquidatable.
|
||||||
validate_liquidation_results(
|
# validate_liquidation_results(
|
||||||
["0", "0", "0", "0", "1000"],
|
# ["0", "0", "0", "0", "1000"],
|
||||||
["0", "0", "0", "180", "0"],
|
# ["0", "0", "0", "180", "0"],
|
||||||
[None, None, None, None, None],
|
# [None, None, None, None, None],
|
||||||
[
|
# [
|
||||||
(
|
# (
|
||||||
["2000", "30000", "40", "5.2", "1"],
|
# ["2000", "30000", "40", "5.2", "1"],
|
||||||
"64",
|
# "64",
|
||||||
True
|
# True
|
||||||
),
|
# ),
|
||||||
(
|
# (
|
||||||
["2000", "30000", "40", "5", "1"],
|
# ["2000", "30000", "40", "5", "1"],
|
||||||
"100",
|
# "100",
|
||||||
False
|
# False
|
||||||
)
|
# )
|
||||||
]
|
# ]
|
||||||
)
|
# )
|
||||||
|
|
||||||
|
|
||||||
def test_open_orders_balance_not_liquidatable():
|
# def test_open_orders_balance_not_liquidatable():
|
||||||
# SRM OO account has 10 SRM in it
|
# # SRM OO account has 10 SRM in it
|
||||||
validate_liquidation_results(
|
# validate_liquidation_results(
|
||||||
["0", "0", "0", "0", "1000"],
|
# ["0", "0", "0", "0", "1000"],
|
||||||
["0", "0", "0", "190", "0"],
|
# ["0", "0", "0", "190", "0"],
|
||||||
[None, None, None, mock_open_orders(base_token_total=Decimal(10)), None],
|
# [None, None, None, mock_open_orders(base_token_total=Decimal(10)), None],
|
||||||
[
|
# [
|
||||||
(
|
# (
|
||||||
["2000", "30000", "40", "5", "1"],
|
# ["2000", "30000", "40", "5", "1"],
|
||||||
"100",
|
# "100",
|
||||||
False
|
# False
|
||||||
)
|
# )
|
||||||
]
|
# ]
|
||||||
)
|
# )
|
||||||
|
|
||||||
|
|
||||||
def test_open_orders_balance_liquidatable():
|
# def test_open_orders_balance_liquidatable():
|
||||||
# SRM OO account has only 9 SRM in it.
|
# # SRM OO account has only 9 SRM in it.
|
||||||
# Assets (1045) / Liabiities (950) = collateral ratio of exactly 1.1
|
# # Assets (1045) / Liabiities (950) = collateral ratio of exactly 1.1
|
||||||
validate_liquidation_results(
|
# validate_liquidation_results(
|
||||||
["0", "0", "0", "0", "1000"],
|
# ["0", "0", "0", "0", "1000"],
|
||||||
["0", "0", "0", "190", "0"],
|
# ["0", "0", "0", "190", "0"],
|
||||||
[None, None, None, mock_open_orders(base_token_total=Decimal(9)), None],
|
# [None, None, None, mock_open_orders(base_token_total=Decimal(9)), None],
|
||||||
[
|
# [
|
||||||
(
|
# (
|
||||||
["2000", "30000", "40", "5", "1"],
|
# ["2000", "30000", "40", "5", "1"],
|
||||||
"95",
|
# "95",
|
||||||
True
|
# True
|
||||||
)
|
# )
|
||||||
]
|
# ]
|
||||||
)
|
# )
|
||||||
|
|
||||||
|
|
||||||
def test_open_orders_referral_fee_not_liquidatable():
|
# def test_open_orders_referral_fee_not_liquidatable():
|
||||||
# Figures are exactly the same as the test_open_orders_balance_liquidatable() test above,
|
# # Figures are exactly the same as the test_open_orders_balance_liquidatable() test above,
|
||||||
# except for the referrer_rebate_accrued value. If it's not taken into account, the
|
# # except for the referrer_rebate_accrued value. If it's not taken into account, the
|
||||||
# margin account is liquidatable.
|
# # margin account is liquidatable.
|
||||||
validate_liquidation_results(
|
# validate_liquidation_results(
|
||||||
["0", "0", "0", "0", "1000"],
|
# ["0", "0", "0", "0", "1000"],
|
||||||
["0", "0", "0", "190", "0"],
|
# ["0", "0", "0", "190", "0"],
|
||||||
[None, None, None, mock_open_orders(base_token_total=Decimal(9), referrer_rebate_accrued=Decimal("0.1")), None],
|
# [None, None, None, mock_open_orders(base_token_total=Decimal(9), referrer_rebate_accrued=Decimal("0.1")), None],
|
||||||
[
|
# [
|
||||||
(
|
# (
|
||||||
["2000", "30000", "40", "5", "1"],
|
# ["2000", "30000", "40", "5", "1"],
|
||||||
"95.1", # The 0.1 referrer rebate is the difference between non-liquidation and iquidation.
|
# "95.1", # The 0.1 referrer rebate is the difference between non-liquidation and iquidation.
|
||||||
False
|
# False
|
||||||
)
|
# )
|
||||||
]
|
# ]
|
||||||
)
|
# )
|
||||||
|
|
||||||
|
|
||||||
def test_open_orders_bigger_referral_fee_not_liquidatable():
|
# def test_open_orders_bigger_referral_fee_not_liquidatable():
|
||||||
# 900 USDC + 100.1 USDC referrer rebate should be equivalent to the above
|
# # 900 USDC + 100.1 USDC referrer rebate should be equivalent to the above
|
||||||
# test_open_orders_referral_fee_not_liquidatable test.
|
# # test_open_orders_referral_fee_not_liquidatable test.
|
||||||
validate_liquidation_results(
|
# validate_liquidation_results(
|
||||||
["0", "0", "0", "0", "900"],
|
# ["0", "0", "0", "0", "900"],
|
||||||
["0", "0", "0", "190", "0"],
|
# ["0", "0", "0", "190", "0"],
|
||||||
[None, None, None, mock_open_orders(base_token_total=Decimal(
|
# [None, None, None, mock_open_orders(base_token_total=Decimal(
|
||||||
9), referrer_rebate_accrued=Decimal("100.1")), None],
|
# 9), referrer_rebate_accrued=Decimal("100.1")), None],
|
||||||
[
|
# [
|
||||||
(
|
# (
|
||||||
["2000", "30000", "40", "5", "1"],
|
# ["2000", "30000", "40", "5", "1"],
|
||||||
"95.1", # 0.1 of the referrer rebate is the difference between non-liquidation and iquidation.
|
# "95.1", # 0.1 of the referrer rebate is the difference between non-liquidation and iquidation.
|
||||||
False
|
# False
|
||||||
)
|
# )
|
||||||
]
|
# ]
|
||||||
)
|
# )
|
||||||
|
|
||||||
|
|
||||||
def test_open_orders_bigger_referral_fee_liquidatable():
|
# def test_open_orders_bigger_referral_fee_liquidatable():
|
||||||
# 900 USDC + 100.1 USDC referrer rebate should be equivalent to the above
|
# # 900 USDC + 100.1 USDC referrer rebate should be equivalent to the above
|
||||||
# test_open_orders_referral_fee_not_liquidatable test.
|
# # test_open_orders_referral_fee_not_liquidatable test.
|
||||||
validate_liquidation_results(
|
# validate_liquidation_results(
|
||||||
["0", "0", "0", "0", "900"],
|
# ["0", "0", "0", "0", "900"],
|
||||||
["0", "0", "0", "190", "0"],
|
# ["0", "0", "0", "190", "0"],
|
||||||
[None, None, None, mock_open_orders(base_token_total=Decimal(
|
# [None, None, None, mock_open_orders(base_token_total=Decimal(
|
||||||
9), referrer_rebate_accrued=Decimal("100")), None],
|
# 9), referrer_rebate_accrued=Decimal("100")), None],
|
||||||
[
|
# [
|
||||||
(
|
# (
|
||||||
["2000", "30000", "40", "5", "1"],
|
# ["2000", "30000", "40", "5", "1"],
|
||||||
"95", # Just 0.1 more in the referrer rebate would make it non-liquidatable.
|
# "95", # Just 0.1 more in the referrer rebate would make it non-liquidatable.
|
||||||
True
|
# True
|
||||||
)
|
# )
|
||||||
]
|
# ]
|
||||||
)
|
# )
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
from .context import mango
|
|
||||||
|
|
||||||
|
|
||||||
def test_constructor():
|
|
||||||
initialized: bool = True
|
|
||||||
group: bool = True
|
|
||||||
margin_account: bool = True
|
|
||||||
srm_account: bool = True
|
|
||||||
actual = mango.MangoAccountFlags(mango.Version.V1, initialized, group,
|
|
||||||
margin_account, srm_account)
|
|
||||||
assert actual is not None
|
|
||||||
assert actual.logger is not None
|
|
||||||
assert actual.version == mango.Version.V1
|
|
||||||
assert actual.initialized == initialized
|
|
||||||
assert actual.group == group
|
|
||||||
assert actual.margin_account == margin_account
|
|
||||||
assert actual.srm_account == srm_account
|
|
||||||
|
|
||||||
actual2 = mango.MangoAccountFlags(mango.Version.V2, not initialized, not group,
|
|
||||||
not margin_account, not srm_account)
|
|
||||||
assert actual2 is not None
|
|
||||||
assert actual2.logger is not None
|
|
||||||
assert actual2.version == mango.Version.V2
|
|
||||||
assert actual2.initialized == (not initialized)
|
|
||||||
assert actual2.group == (not group)
|
|
||||||
assert actual2.margin_account == (not margin_account)
|
|
||||||
assert actual2.srm_account == (not srm_account)
|
|
|
@ -1,31 +0,0 @@
|
||||||
from .context import mango
|
|
||||||
from .fakes import fake_account_info, fake_seeded_public_key, fake_token
|
|
||||||
|
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
|
|
||||||
def test_construction():
|
|
||||||
account_flags = mango.MangoAccountFlags(mango.Version.V1, True, False, True, False)
|
|
||||||
has_borrows = False
|
|
||||||
mango_group = fake_seeded_public_key("mango group")
|
|
||||||
owner = fake_seeded_public_key("owner")
|
|
||||||
being_liquidated = False
|
|
||||||
token = fake_token()
|
|
||||||
deposits = [mango.TokenValue(token, Decimal(0)), mango.TokenValue(
|
|
||||||
token, Decimal(0)), mango.TokenValue(token, Decimal(0))]
|
|
||||||
borrows = [mango.TokenValue(token, Decimal(0)), mango.TokenValue(
|
|
||||||
token, Decimal(0)), mango.TokenValue(token, Decimal(0))]
|
|
||||||
open_orders = [None, None]
|
|
||||||
actual = mango.MarginAccount(fake_account_info(), mango.Version.V1, account_flags,
|
|
||||||
has_borrows, mango_group, owner, being_liquidated,
|
|
||||||
deposits, borrows, open_orders)
|
|
||||||
assert actual is not None
|
|
||||||
assert actual.logger is not None
|
|
||||||
assert actual.version == mango.Version.V1
|
|
||||||
assert actual.account_flags == account_flags
|
|
||||||
assert actual.has_borrows == has_borrows
|
|
||||||
assert actual.mango_group == mango_group
|
|
||||||
assert actual.owner == owner
|
|
||||||
assert actual.deposits == deposits
|
|
||||||
assert actual.borrows == borrows
|
|
||||||
assert actual.open_orders == open_orders
|
|
|
@ -1,14 +0,0 @@
|
||||||
from .context import mango
|
|
||||||
from .fakes import fake_index, fake_seeded_public_key, fake_token
|
|
||||||
|
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
|
|
||||||
def test_constructor():
|
|
||||||
base = mango.BasketToken(fake_token(), fake_seeded_public_key("base vault"), fake_index())
|
|
||||||
quote = mango.BasketToken(fake_token(), fake_seeded_public_key("quote vault"), fake_index())
|
|
||||||
spot_market = mango.SpotMarket(fake_seeded_public_key("spot market"), base, quote)
|
|
||||||
actual = mango.MarketMetadata("FAKE/MKT", fake_seeded_public_key("market metadata"),
|
|
||||||
base, quote, spot_market, fake_seeded_public_key("oracle"), Decimal(7))
|
|
||||||
assert actual is not None
|
|
||||||
assert actual.logger is not None
|
|
|
@ -10,7 +10,7 @@ def test_constructor():
|
||||||
market = fake_public_key()
|
market = fake_public_key()
|
||||||
owner = fake_public_key()
|
owner = fake_public_key()
|
||||||
|
|
||||||
flags = mango.SerumAccountFlags(mango.Version.V1, True, False, True, False, False, False, False, False)
|
flags = mango.AccountFlags(mango.Version.V1, True, False, True, False, False, False, False, False)
|
||||||
actual = mango.OpenOrders(account_info, mango.Version.V1, program_id, flags, market,
|
actual = mango.OpenOrders(account_info, mango.Version.V1, program_id, flags, market,
|
||||||
owner, Decimal(0), Decimal(0), Decimal(0), Decimal(0),
|
owner, Decimal(0), Decimal(0), Decimal(0), Decimal(0),
|
||||||
Decimal(0), Decimal(0), [], [], Decimal(0))
|
Decimal(0), Decimal(0), [], [], Decimal(0))
|
||||||
|
|
|
@ -8,7 +8,7 @@ import typing
|
||||||
def test_constructor():
|
def test_constructor():
|
||||||
name: str = "Test"
|
name: str = "Test"
|
||||||
func: typing.Callable = lambda: 1
|
func: typing.Callable = lambda: 1
|
||||||
pauses: typing.List[Decimal] = [Decimal(2)]
|
pauses: typing.Sequence[Decimal] = [Decimal(2)]
|
||||||
actual = mango.RetryWithPauses(name, func, pauses)
|
actual = mango.RetryWithPauses(name, func, pauses)
|
||||||
assert actual is not None
|
assert actual is not None
|
||||||
assert actual.logger is not None
|
assert actual.logger is not None
|
||||||
|
@ -23,7 +23,7 @@ def test_0_retry():
|
||||||
|
|
||||||
# Number of retries is the number of pauses - 1.
|
# Number of retries is the number of pauses - 1.
|
||||||
# The retrier only pauses if an exception is raised.
|
# The retrier only pauses if an exception is raised.
|
||||||
pauses: typing.List[Decimal] = [Decimal(0)]
|
pauses: typing.Sequence[Decimal] = [Decimal(0)]
|
||||||
|
|
||||||
class FuncScope:
|
class FuncScope:
|
||||||
called: int = 0
|
called: int = 0
|
||||||
|
@ -48,7 +48,7 @@ def test_1_retry():
|
||||||
|
|
||||||
# Number of retries is the number of pauses - 1.
|
# Number of retries is the number of pauses - 1.
|
||||||
# The retrier only pauses if an exception is raised.
|
# The retrier only pauses if an exception is raised.
|
||||||
pauses: typing.List[Decimal] = [Decimal(0), Decimal(0)]
|
pauses: typing.Sequence[Decimal] = [Decimal(0), Decimal(0)]
|
||||||
|
|
||||||
class FuncScope:
|
class FuncScope:
|
||||||
called: int = 0
|
called: int = 0
|
||||||
|
@ -73,7 +73,7 @@ def test_3_retries():
|
||||||
|
|
||||||
# Number of retries is the number of pauses - 1.
|
# Number of retries is the number of pauses - 1.
|
||||||
# The retrier only pauses if an exception is raised.
|
# The retrier only pauses if an exception is raised.
|
||||||
pauses: typing.List[Decimal] = [Decimal(0), Decimal(0), Decimal(0), Decimal(0)]
|
pauses: typing.Sequence[Decimal] = [Decimal(0), Decimal(0), Decimal(0), Decimal(0)]
|
||||||
|
|
||||||
class FuncScope:
|
class FuncScope:
|
||||||
called: int = 0
|
called: int = 0
|
||||||
|
@ -98,7 +98,7 @@ def test_with_context():
|
||||||
|
|
||||||
# Number of retries is the number of pauses - 1.
|
# Number of retries is the number of pauses - 1.
|
||||||
# The retrier only pauses if an exception is raised.
|
# The retrier only pauses if an exception is raised.
|
||||||
pauses: typing.List[Decimal] = [Decimal(0), Decimal(0), Decimal(0), Decimal(0)]
|
pauses: typing.Sequence[Decimal] = [Decimal(0), Decimal(0), Decimal(0), Decimal(0)]
|
||||||
|
|
||||||
class FuncScope:
|
class FuncScope:
|
||||||
called: int = 0
|
called: int = 0
|
||||||
|
|
|
@ -24,21 +24,21 @@ def test_transaction_instruction_constructor():
|
||||||
|
|
||||||
def test_transaction_scout_constructor():
|
def test_transaction_scout_constructor():
|
||||||
timestamp: datetime = datetime.now()
|
timestamp: datetime = datetime.now()
|
||||||
signatures: typing.List[str] = ["Signature1", "Signature2"]
|
signatures: typing.Sequence[str] = ["Signature1", "Signature2"]
|
||||||
succeeded: bool = True
|
succeeded: bool = True
|
||||||
group_name: str = "BTC_ETH_USDT"
|
group_name: str = "BTC_ETH_USDT"
|
||||||
account1: PublicKey = fake_seeded_public_key("account 1")
|
account1: PublicKey = fake_seeded_public_key("account 1")
|
||||||
account2: PublicKey = fake_seeded_public_key("account 2")
|
account2: PublicKey = fake_seeded_public_key("account 2")
|
||||||
account3: PublicKey = fake_seeded_public_key("account 3")
|
account3: PublicKey = fake_seeded_public_key("account 3")
|
||||||
accounts: typing.List[PublicKey] = [account1, account2, account3]
|
accounts: typing.Sequence[PublicKey] = [account1, account2, account3]
|
||||||
instructions: typing.List[str] = ["Instruction"]
|
instructions: typing.Sequence[str] = ["Instruction"]
|
||||||
messages: typing.List[str] = ["Message 1", "Message 2"]
|
messages: typing.Sequence[str] = ["Message 1", "Message 2"]
|
||||||
token = fake_token()
|
token = fake_token()
|
||||||
token_value = mango.TokenValue(token, Decimal(28))
|
token_value = mango.TokenValue(token, Decimal(28))
|
||||||
owner = fake_seeded_public_key("owner")
|
owner = fake_seeded_public_key("owner")
|
||||||
owned_token_value = mango.OwnedTokenValue(owner, token_value)
|
owned_token_value = mango.OwnedTokenValue(owner, token_value)
|
||||||
pre_token_balances: typing.List[mango.OwnedTokenValue] = [owned_token_value]
|
pre_token_balances: typing.Sequence[mango.OwnedTokenValue] = [owned_token_value]
|
||||||
post_token_balances: typing.List[mango.OwnedTokenValue] = [owned_token_value]
|
post_token_balances: typing.Sequence[mango.OwnedTokenValue] = [owned_token_value]
|
||||||
actual = mango.TransactionScout(timestamp, signatures, succeeded, group_name, accounts,
|
actual = mango.TransactionScout(timestamp, signatures, succeeded, group_name, accounts,
|
||||||
instructions, messages, pre_token_balances, post_token_balances)
|
instructions, messages, pre_token_balances, post_token_balances)
|
||||||
assert actual is not None
|
assert actual is not None
|
||||||
|
|
Loading…
Reference in New Issue