Added Account.build_redeem_pnl_instructions_for_market() to build redeem instructions.

* Simpler access is through Account.redeem_pnl_for_perp_market() and Account.redeem_all_perp_pnl()
* Added redeem-pnl command to allow redemption via command line and to show how to exercise these methods.
This commit is contained in:
Geoff Taylor 2022-03-14 17:47:30 +00:00
parent 66446fa2c9
commit b5916fcb50
5 changed files with 417 additions and 2 deletions

97
bin/redeem-pnl Executable file
View File

@ -0,0 +1,97 @@
#!/usr/bin/env python3
import argparse
import os
import os.path
import sys
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="redeems PnL for a Mango account")
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument(
"--market", type=str, help="perp market symbol to redeem PnL (e.g. ETH-PERP)"
)
parser.add_argument(
"--all",
action="store_true",
default=False,
help="redeem all MNGO in all perp markets in the account",
)
parser.add_argument(
"--account-address",
type=PublicKey,
help="address of the specific account to use, if more than one available",
)
parser.add_argument(
"--dry-run",
action="store_true",
default=False,
help="runs as read-only and does not perform any transactions",
)
parser.add_argument(
"--wait",
action="store_true",
default=False,
help="wait until the transactions are confirmed",
)
args: argparse.Namespace = mango.parse_args(parser)
if (not args.all) and (args.market is None):
raise Exception(
"Must specify either an individual market (using --market) or use --all for all markets"
)
with mango.ContextBuilder.from_command_line_parameters(args) as context:
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
group = mango.Group.load(context, context.group_address)
cache = mango.Cache.load(context, group.cache)
account = mango.Account.load_for_owner_by_address(
context, wallet.address, group, args.account_address
)
if args.all:
signatures = account.redeem_all_perp_pnl(context, wallet, group, cache)
else:
group_slots = [
gs
for gs in group.slots
if mango.Market.symbols_match(gs.perp_market_symbol, args.market)
]
if len(group_slots) != 1:
raise Exception(
f"Could not find perp market slot with symbol '{args.market}'"
)
group_slot = group_slots[0]
slot = account.slots[group_slot.index]
perp_market_cache = group.perp_market_cache_from_cache(
cache, slot.base_instrument
)
if perp_market_cache is None:
raise Exception(
f"Could not find perp market cache for {slot.base_instrument.symbol}"
)
price = group.token_price_from_cache(cache, slot.base_instrument)
signatures = account.redeem_pnl_for_perp_market(
context, wallet, group, slot, perp_market_cache, price
)
if args.wait:
mango.output("Waiting on transaction signatures:")
mango.output(mango.indent_collection_as_str(signatures, 1))
results = mango.WebSocketTransactionMonitor.wait_for_all(
context.client.cluster_ws_url, signatures
)
mango.output("Transaction results:")
mango.output(mango.indent_collection_as_str(results, 1))
else:
mango.output("Transaction signatures:")
mango.output(mango.indent_collection_as_str(signatures, 1))

View File

@ -105,6 +105,12 @@ from .instructions import (
from .instructions import (
build_mango_set_referrer_memory_instructions as build_mango_set_referrer_memory_instructions,
)
from .instructions import (
build_mango_settle_fees_instructions as build_mango_settle_fees_instructions,
)
from .instructions import (
build_mango_settle_pnl_instructions as build_mango_settle_pnl_instructions,
)
from .instructions import (
build_mango_update_funding_instructions as build_mango_update_funding_instructions,
)

View File

@ -30,6 +30,8 @@ from .encoding import encode_key
from .group import Group, GroupSlot, GroupSlotPerpMarket
from .instructions import (
build_mango_deposit_instructions,
build_mango_settle_fees_instructions,
build_mango_settle_pnl_instructions,
build_mango_withdraw_instructions,
)
from .instrumentvalue import InstrumentValue
@ -39,6 +41,7 @@ from .observables import Disposable
from .openorders import OpenOrders
from .orders import Side
from .perpaccount import PerpAccount
from .perpmarketdetails import PerpMarketDetails
from .perpopenorders import PerpOpenOrders
from .placedorder import PlacedOrder
from .tokens import Instrument, Token
@ -167,6 +170,15 @@ class AccountSlot:
def raw_net_value(self) -> Decimal:
return self.raw_deposit - self.raw_borrow
def pnl(
self,
perp_market_cache: PerpMarketCache,
price: InstrumentValue,
) -> Decimal:
if self.perp_account is None:
return Decimal(0)
return self.perp_account.pnl(perp_market_cache, price)
def __str__(self) -> str:
perp_account: str = "None"
if self.perp_account is not None:
@ -697,6 +709,201 @@ class Account(AddressableAccount):
all_instructions = signers + create_ata + withdraw
return all_instructions.execute(context)
def redeem_all_perp_pnl(
self,
context: Context,
wallet: Wallet,
group: Group,
cache: Cache,
) -> typing.Sequence[str]:
all_accounts = Account.load_all(context, group)
self._logger.debug(f"Fetched {len(all_accounts)} Mango Accounts.")
instructions = CombinableInstructions.from_wallet(wallet)
for slot in self.slots:
if (slot.perp_account is not None) and (not slot.perp_account.empty):
self._logger.debug(
f"Redeeming perp account for {slot.base_instrument.symbol}"
)
price = group.token_price_from_cache(cache, slot.base_instrument)
perp_market_cache = group.perp_market_cache_from_cache(
cache, slot.base_instrument
)
if perp_market_cache is None:
raise Exception(
f"Could not load perp market cache for {slot.base_instrument.symbol} despite having a perp account for it."
)
instructions += self.build_redeem_pnl_instructions_for_market(
context, group, all_accounts, slot, perp_market_cache, price
)
else:
self._logger.debug(f"No perp account for {slot.base_instrument.symbol}")
self._logger.debug(
f"Have {len(instructions.instructions)} instructions to send for all perp markets."
)
return instructions.execute(context)
def redeem_pnl_for_perp_market(
self,
context: Context,
wallet: Wallet,
group: Group,
slot: AccountSlot,
perp_market_cache: PerpMarketCache,
price: InstrumentValue,
) -> typing.Sequence[str]:
group_slot = group.slots[slot.index]
if group_slot.perp_market is None:
return []
if slot.perp_account is None:
return []
pnl = slot.perp_account.pnl(perp_market_cache, price)
if pnl == 0:
# No PnL to redeem
return []
self._logger.debug(
f"Trying to settle {pnl:,.8f} {group_slot.quote_token_bank.token.symbol}."
)
instructions = CombinableInstructions.from_wallet(wallet)
all_accounts = Account.load_all(context, group)
self._logger.debug(f"Fetched {len(all_accounts)} Mango Accounts.")
instructions += self.build_redeem_pnl_instructions_for_market(
context, group, all_accounts, slot, perp_market_cache, price
)
return instructions.execute(context)
def build_redeem_pnl_instructions_for_market(
self,
context: Context,
group: Group,
all_accounts: typing.Sequence["Account"],
slot: AccountSlot,
perp_market_cache: PerpMarketCache,
price: InstrumentValue,
) -> CombinableInstructions:
group_slot = group.slots[slot.index]
if group_slot.perp_market is None:
self._logger.debug(f"No perp market for {slot.base_instrument.symbol}")
return CombinableInstructions.empty()
if slot.perp_account is None:
self._logger.debug(f"No perp account for {slot.base_instrument.symbol}")
return CombinableInstructions.empty()
pnl = slot.perp_account.pnl(perp_market_cache, price)
if pnl == 0:
# No PnL to redeem
return CombinableInstructions.empty()
self._logger.debug(
f"Trying to settle {pnl:,.8f} {group_slot.quote_token_bank.token.symbol} on {group_slot.perp_market_symbol}."
)
pnl_is_negative: bool = False
if pnl < 0:
pnl_is_negative = True
quote_root_bank = group.shared_quote.ensure_root_bank(context)
instructions = CombinableInstructions.empty()
# If negative, build settle fees instruction
if pnl_is_negative:
perp_market = PerpMarketDetails.load(
context, group_slot.perp_market.address, group
)
quote_node_bank = quote_root_bank.pick_node_bank(context)
self._logger.debug("Adding SettleFees instruction.")
instructions += build_mango_settle_fees_instructions(
context,
group,
perp_market,
self,
quote_root_bank,
quote_node_bank,
)
all_accounts_except_self = filter(
lambda acc: acc.address != self.address,
all_accounts,
)
# Filter out all accounts with zero or same-signed PnL
if pnl_is_negative:
filtered = list(
filter(
lambda acc: acc.slots[slot.index].pnl(perp_market_cache, price) > 0,
all_accounts_except_self,
)
)
else:
filtered = list(
filter(
lambda acc: acc.slots[slot.index].pnl(perp_market_cache, price) < 0,
all_accounts_except_self,
)
)
self._logger.debug(
f"Removed irrelevent accounts - now have {len(filtered)} Mango Accounts."
)
# Now we have a filtered list, sort it
sorted_accounts = sorted(
filtered,
key=lambda acc: acc.slots[slot.index].pnl(perp_market_cache, price),
reverse=pnl_is_negative,
)
# Add 5% to our collection so we're sure we're sending enough accounts even if
# the price has shifted. (This doesn't affect the redemption value.)
target_pnl = (pnl * Decimal("1.01")).copy_abs()
# Loop over top N accounts until we have redeemed enough or we run out of accounts.
collected = Decimal(0)
for other in sorted_accounts:
other_perp_account = other.slots[slot.index].perp_account
if other_perp_account is not None:
other_pnl = other_perp_account.pnl(perp_market_cache, price)
# Build instruction
instructions += build_mango_settle_pnl_instructions(
context,
group,
group_slot,
self,
other,
quote_root_bank,
)
collected += other_pnl
self._logger.debug(
f"Collected {collected:,.8f} out of {target_pnl:,.8f} - added {other_pnl:,.8f} from {other.address}"
)
# Add 1% to our collection so we're sure we're sending enough accounts even if
# the price has shifted. (This doesn't affect the redemption value.)
if collected.copy_abs() > target_pnl:
self._logger.debug(
f"Done. Have collected {collected:,.8f} to redeem against."
)
break
self._logger.debug(
f"Have {len(instructions.instructions)} instructions to send for {group_slot.perp_market_symbol}."
)
return instructions
def slot_by_instrument_or_none(
self, instrument: Instrument
) -> typing.Optional[AccountSlot]:

View File

@ -108,6 +108,7 @@ class IGroupSlot(typing.Protocol):
class IGroup(typing.Protocol):
cache: PublicKey
signer_key: PublicKey
fees_vault: PublicKey
shared_quote: TokenBank
@property
@ -1712,3 +1713,81 @@ def build_mango_update_funding_instructions(
data=layouts.UPDATE_FUNDING.build({}),
)
return CombinableInstructions(signers=[], instructions=[instruction])
def build_mango_settle_fees_instructions(
context: Context,
group: IGroup,
perp_market_details: PerpMarketDetails,
account: IAccount,
root_bank: RootBank,
node_bank: NodeBank,
) -> CombinableInstructions:
# /// Take an account that has losses in the selected perp market to account for fees_accrued
# ///
# /// Accounts expected: 10
# /// 0. `[]` mango_group_ai - MangoGroup
# /// 1. `[]` mango_cache_ai - MangoCache
# /// 2. `[writable]` perp_market_ai - PerpMarket
# /// 3. `[writable]` mango_account_ai - MangoAccount
# /// 4. `[]` root_bank_ai - RootBank
# /// 5. `[writable]` node_bank_ai - NodeBank
# /// 6. `[writable]` bank_vault_ai - ?
# /// 7. `[writable]` fees_vault_ai - fee vault owned by mango DAO token governance
# /// 8. `[]` signer_ai - Group Signer Account
# /// 9. `[]` token_prog_ai - Token Program Account
instruction = TransactionInstruction(
keys=[
AccountMeta(is_signer=False, is_writable=False, pubkey=group.address),
AccountMeta(is_signer=False, is_writable=False, pubkey=group.cache),
AccountMeta(
is_signer=False, is_writable=True, pubkey=perp_market_details.address
),
AccountMeta(is_signer=False, is_writable=True, pubkey=account.address),
AccountMeta(is_signer=False, is_writable=False, pubkey=root_bank.address),
AccountMeta(is_signer=False, is_writable=True, pubkey=node_bank.address),
AccountMeta(is_signer=False, is_writable=True, pubkey=node_bank.vault),
AccountMeta(is_signer=False, is_writable=True, pubkey=group.fees_vault),
AccountMeta(is_signer=False, is_writable=False, pubkey=group.signer_key),
AccountMeta(is_signer=False, is_writable=False, pubkey=TOKEN_PROGRAM_ID),
],
program_id=context.mango_program_address,
data=layouts.SETTLE_FEES.build({}),
)
return CombinableInstructions(signers=[], instructions=[instruction])
def build_mango_settle_pnl_instructions(
context: Context,
group: IGroup,
group_slot: IGroupSlot,
account_a: IAccount,
account_b: IAccount,
root_bank: RootBank,
) -> CombinableInstructions:
# /// Take two MangoAccounts and settle profits and losses between them for a perp market
# ///
# /// Accounts expected (6):
# const keys = [
# { isSigner: false, isWritable: false, pubkey: mangoGroupPk },
# { isSigner: false, isWritable: true, pubkey: mangoAccountAPk },
# { isSigner: false, isWritable: true, pubkey: mangoAccountBPk },
# { isSigner: false, isWritable: false, pubkey: mangoCachePk },
# { isSigner: false, isWritable: false, pubkey: rootBankPk },
# { isSigner: false, isWritable: true, pubkey: nodeBankPk },
# ];
instruction = TransactionInstruction(
keys=[
AccountMeta(is_signer=False, is_writable=False, pubkey=group.address),
AccountMeta(is_signer=False, is_writable=True, pubkey=account_a.address),
AccountMeta(is_signer=False, is_writable=True, pubkey=account_b.address),
AccountMeta(is_signer=False, is_writable=False, pubkey=group.cache),
AccountMeta(is_signer=False, is_writable=False, pubkey=root_bank.address),
AccountMeta(
is_signer=False, is_writable=True, pubkey=root_bank.node_banks[0]
),
],
program_id=context.mango_program_address,
data=layouts.SETTLE_PNL.build({"market_index": group_slot.index}),
)
return CombinableInstructions(signers=[], instructions=[instruction])

View File

@ -1584,6 +1584,32 @@ UPDATE_ROOT_BANK = construct.Struct(
"variant" / construct.Const(21, construct.BytesInteger(4, swapped=True)),
)
# /// Take two MangoAccounts and settle profits and losses between them for a perp market
# ///
# /// Accounts expected (6):
SETTLE_PNL = construct.Struct(
"variant" / construct.Const(22, construct.BytesInteger(4, swapped=True)),
"market_index" / DecimalAdapter(),
)
# /// Take an account that has losses in the selected perp market to account for fees_accrued
# ///
# /// Accounts expected: 10
# /// 0. `[]` mango_group_ai - MangoGroup
# /// 1. `[]` mango_cache_ai - MangoCache
# /// 2. `[writable]` perp_market_ai - PerpMarket
# /// 3. `[writable]` mango_account_ai - MangoAccount
# /// 4. `[]` root_bank_ai - RootBank
# /// 5. `[writable]` node_bank_ai - NodeBank
# /// 6. `[writable]` bank_vault_ai - ?
# /// 7. `[writable]` fees_vault_ai - fee vault owned by mango DAO token governance
# /// 8. `[]` signer_ai - Group Signer Account
# /// 9. `[]` token_prog_ai - Token Program Account
SETTLE_FEES = construct.Struct(
"variant" / construct.Const(29, construct.BytesInteger(4, swapped=True))
)
# /// Redeem the mngo_accrued in a PerpAccount for MNGO in MangoAccount deposits
# ///
@ -1830,14 +1856,14 @@ InstructionParsersByVariant = {
19: SETTLE_FUNDS, # SETTLE_FUNDS,
20: CANCEL_SPOT_ORDER, # CANCEL_SPOT_ORDER,
21: UPDATE_ROOT_BANK, # UPDATE_ROOT_BANK,
22: UNSPECIFIED, # SETTLE_PNL,
22: SETTLE_PNL, # SETTLE_PNL,
23: UNSPECIFIED, # SETTLE_BORROW,
24: UNSPECIFIED, # FORCE_CANCEL_SPOT_ORDERS,
25: UNSPECIFIED, # FORCE_CANCEL_PERP_ORDERS,
26: UNSPECIFIED, # LIQUIDATE_TOKEN_AND_TOKEN,
27: UNSPECIFIED, # LIQUIDATE_TOKEN_AND_PERP,
28: UNSPECIFIED, # LIQUIDATE_PERP_MARKET,
29: UNSPECIFIED, # SETTLE_FEES,
29: SETTLE_FEES, # SETTLE_FEES,
30: UNSPECIFIED, # RESOLVE_PERP_BANKRUPTCY,
31: UNSPECIFIED, # RESOLVE_TOKEN_BANKRUPTCY,
32: UNSPECIFIED, # INIT_SPOT_OPEN_ORDERS,