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:
parent
66446fa2c9
commit
b5916fcb50
|
@ -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))
|
|
@ -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,
|
||||
)
|
||||
|
|
207
mango/account.py
207
mango/account.py
|
@ -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]:
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue