Switched from autopep8 to black for code formatting. Reformatted all files. Updated dependencies.

This commit is contained in:
Geoff Taylor 2022-02-09 19:31:50 +00:00
parent 9fdccca3a3
commit 5c3b0befa9
233 changed files with 13052 additions and 4563 deletions

View File

@ -1,5 +1,5 @@
[flake8]
ignore = D203,W503
ignore = D203,E203,W503
exclude = .venv,.git,__pycache__,.ipynb_checkpoints,docs
per-file-ignores =
# imported but unused

View File

@ -4,13 +4,7 @@
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.nosetestsEnabled": false,
"python.testing.pytestEnabled": true,
"python.autoComplete.addBrackets": true,
"python.formatting.autopep8Args": [
"--max-line-length",
"120"
],
"python.analysis.completeFunctionParens": true,
"python.pythonPath": "${workspaceFolder}/.venv/bin/python"
"python.formatting.provider": "black"
}

View File

@ -9,18 +9,21 @@ import traceback
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
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="Run the Account Scout to display problems and information about an account.")
description="Run the Account Scout to display problems and information about an account."
)
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--address", type=PublicKey,
help="User's root address for the Account Scout to check (if not provided, the wallet address is used)")
parser.add_argument(
"--address",
type=PublicKey,
help="User's root address for the Account Scout to check (if not provided, the wallet address is used)",
)
args: argparse.Namespace = mango.parse_args(parser)
try:
@ -38,6 +41,10 @@ try:
report = scout.verify_account_prepared_for_group(context, group, address)
mango.output(report)
except Exception as exception:
logging.critical(f"account-scout stopped because of exception: {exception} - {traceback.format_exc()}")
logging.critical(
f"account-scout stopped because of exception: {exception} - {traceback.format_exc()}"
)
except:
logging.critical(f"account-scout stopped because of uncatchable error: {traceback.format_exc()}")
logging.critical(
f"account-scout stopped because of uncatchable error: {traceback.format_exc()}"
)

View File

@ -9,43 +9,63 @@ import typing
from decimal import Decimal
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), '..')))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
def airdrop_token(context: mango.Context, wallet: mango.Wallet, token: mango.Token, faucet: typing.Optional[PublicKey], quantity: Decimal) -> None:
def airdrop_token(
context: mango.Context,
wallet: mango.Wallet,
token: mango.Token,
faucet: typing.Optional[PublicKey],
quantity: Decimal,
) -> None:
if faucet is None:
raise Exception(f"Faucet must be specified for airdropping {token.symbol}")
# This is a root wallet account - get the associated token account
destination: PublicKey = mango.TokenAccount.find_or_create_token_address_to_use(
context, wallet, wallet.address, token)
context, wallet, wallet.address, token
)
signers: mango.CombinableInstructions = mango.CombinableInstructions.from_wallet(wallet)
signers: mango.CombinableInstructions = mango.CombinableInstructions.from_wallet(
wallet
)
mango.output(f"Airdropping {quantity} {token.symbol} to {destination}")
native_quantity = token.shift_to_native(quantity)
airdrop = mango.build_faucet_airdrop_instructions(token.mint, destination, faucet, native_quantity)
airdrop = mango.build_faucet_airdrop_instructions(
token.mint, destination, faucet, native_quantity
)
all_instructions = signers + airdrop
transaction_ids = all_instructions.execute(context)
mango.output("Transaction IDs:", transaction_ids)
def airdrop_sol(context: mango.Context, wallet: mango.Wallet, token: mango.Token, quantity: Decimal) -> None:
def airdrop_sol(
context: mango.Context, wallet: mango.Wallet, token: mango.Token, quantity: Decimal
) -> None:
mango.output(f"Airdropping {quantity} {token.symbol} to {wallet.address}")
lamports = token.shift_to_native(quantity)
response = context.client.compatible_client.request_airdrop(wallet.address, int(lamports))
response = context.client.compatible_client.request_airdrop(
wallet.address, int(lamports)
)
mango.output("Transaction IDs:", [response["result"]])
parser = argparse.ArgumentParser(description="mint SPL tokens to your wallet")
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--symbol", type=str, required=True, help="token symbol to airdrop (e.g. USDC)")
parser.add_argument("--faucet", type=PublicKey, required=False, help="public key of the faucet")
parser.add_argument("--quantity", type=Decimal, required=True, help="quantity token to airdrop")
parser.add_argument(
"--symbol", type=str, required=True, help="token symbol to airdrop (e.g. USDC)"
)
parser.add_argument(
"--faucet", type=PublicKey, required=False, help="public key of the faucet"
)
parser.add_argument(
"--quantity", type=Decimal, required=True, help="quantity token to airdrop"
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)

View File

@ -10,31 +10,58 @@ import typing
from decimal import Decimal
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
def resolve_quantity(token: mango.Token, current_quantity: Decimal, target_quantity: mango.TargetBalance, price: Decimal) -> mango.InstrumentValue:
def resolve_quantity(
token: mango.Token,
current_quantity: Decimal,
target_quantity: mango.TargetBalance,
price: Decimal,
) -> mango.InstrumentValue:
current_value: Decimal = current_quantity * price
resolved_value_to_keep: mango.InstrumentValue = target_quantity.resolve(token, price, current_value)
resolved_value_to_keep: mango.InstrumentValue = target_quantity.resolve(
token, price, current_value
)
return resolved_value_to_keep
parser = argparse.ArgumentParser(
description="Balance the value of tokens in a Mango Markets account to specific values or percentages.")
description="Balance the value of tokens in a Mango Markets account to specific values or percentages."
)
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--target", type=mango.parse_target_balance, action="append", required=True,
help="token symbol plus target value or percentage, separated by a colon (e.g. 'ETH:2.5')")
parser.add_argument("--max-slippage", type=Decimal, default=Decimal("0.05"),
help="maximum slippage allowed for the IOC order price")
parser.add_argument("--action-threshold", type=Decimal, default=Decimal("0.01"),
help="fraction of total wallet value a trade must be above to be carried out")
parser.add_argument("--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(
"--target",
type=mango.parse_target_balance,
action="append",
required=True,
help="token symbol plus target value or percentage, separated by a colon (e.g. 'ETH:2.5')",
)
parser.add_argument(
"--max-slippage",
type=Decimal,
default=Decimal("0.05"),
help="maximum slippage allowed for the IOC order price",
)
parser.add_argument(
"--action-threshold",
type=Decimal,
default=Decimal("0.01"),
help="fraction of total wallet value a trade must be above to be carried out",
)
parser.add_argument(
"--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",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
@ -42,7 +69,9 @@ wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
action_threshold = args.action_threshold
max_slippage = args.max_slippage
group = mango.Group.load(context, context.group_address)
account = mango.Account.load_for_owner_by_address(context, wallet.address, group, args.account_address)
account = mango.Account.load_for_owner_by_address(
context, wallet.address, group, args.account_address
)
targets: typing.Sequence[mango.TargetBalance] = args.target
logging.info(f"Targets: {targets}")
@ -50,13 +79,17 @@ logging.info(f"Targets: {targets}")
if args.dry_run:
trade_executor: mango.TradeExecutor = mango.NullTradeExecutor()
else:
trade_executor = mango.ImmediateTradeExecutor(context, wallet, account, max_slippage)
trade_executor = mango.ImmediateTradeExecutor(
context, wallet, account, max_slippage
)
prices: typing.List[mango.InstrumentValue] = []
oracle_provider: mango.OracleProvider = mango.create_oracle_provider(context, "market")
for basket_token in account.slots:
if basket_token is not None:
market_symbol: str = f"{basket_token.base_instrument.symbol}/{group.shared_quote_token.symbol}"
market_symbol: str = (
f"{basket_token.base_instrument.symbol}/{group.shared_quote_token.symbol}"
)
market = context.market_lookup.find_by_symbol(market_symbol)
if market is None:
raise Exception(f"Could not find market {market_symbol}")
@ -67,7 +100,9 @@ for basket_token in account.slots:
prices += [mango.InstrumentValue(basket_token.base_instrument, price.mid_price)]
prices += [mango.InstrumentValue(group.shared_quote_token, Decimal(1))]
account_balancer = mango.LiveAccountBalancer(account, group, trade_executor, targets, action_threshold)
account_balancer = mango.LiveAccountBalancer(
account, group, trade_executor, targets, action_threshold
)
account_balancer.balance(context, prices)
logging.info("Balancing completed.")

View File

@ -9,25 +9,47 @@ import typing
from decimal import Decimal
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
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="Balance the value of tokens in a Mango Markets group to specific values or percentages.")
description="Balance the value of tokens in a Mango Markets group to specific values or percentages."
)
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--target", type=mango.parse_fixed_target_balance, action="append", required=True,
help="token symbol plus target value, separated by a colon (e.g. 'ETH:2.5')")
parser.add_argument("--action-threshold", type=Decimal, default=Decimal("0.01"),
help="fraction of total wallet value a trade must be above to be carried out")
parser.add_argument("--adjustment-factor", type=Decimal, default=Decimal("0.05"),
help="factor by which to adjust the SELL price (akin to maximum slippage)")
parser.add_argument("--quote-symbol", type=str, default="USDC", help="quote token symbol to use for markets")
parser.add_argument("--dry-run", action="store_true", default=False,
help="runs as read-only and does not perform any transactions")
parser.add_argument(
"--target",
type=mango.parse_fixed_target_balance,
action="append",
required=True,
help="token symbol plus target value, separated by a colon (e.g. 'ETH:2.5')",
)
parser.add_argument(
"--action-threshold",
type=Decimal,
default=Decimal("0.01"),
help="fraction of total wallet value a trade must be above to be carried out",
)
parser.add_argument(
"--adjustment-factor",
type=Decimal,
default=Decimal("0.05"),
help="factor by which to adjust the SELL price (akin to maximum slippage)",
)
parser.add_argument(
"--quote-symbol",
type=str,
default="USDC",
help="quote token symbol to use for markets",
)
parser.add_argument(
"--dry-run",
action="store_true",
default=False,
help="runs as read-only and does not perform any transactions",
)
args: argparse.Namespace = mango.parse_args(parser)
context: mango.Context = mango.ContextBuilder.from_command_line_parameters(args)
@ -43,9 +65,13 @@ logging.info(f"Targets: {targets}")
if args.dry_run:
trade_executor: mango.TradeExecutor = mango.NullTradeExecutor()
else:
trade_executor = mango.ImmediateTradeExecutor(context, wallet, None, adjustment_factor)
trade_executor = mango.ImmediateTradeExecutor(
context, wallet, None, adjustment_factor
)
quote_instrument: typing.Optional[mango.Instrument] = context.instrument_lookup.find_by_symbol(args.quote_symbol)
quote_instrument: typing.Optional[
mango.Instrument
] = context.instrument_lookup.find_by_symbol(args.quote_symbol)
if quote_instrument is None:
raise Exception(f"Could not find quote token '{args.quote_symbol}.")
quote_token: mango.Token = mango.Token.ensure(quote_instrument)
@ -53,7 +79,9 @@ quote_token: mango.Token = mango.Token.ensure(quote_instrument)
prices: typing.List[mango.InstrumentValue] = []
oracle_provider: mango.OracleProvider = mango.create_oracle_provider(context, "market")
for target in targets:
target_token: typing.Optional[mango.Instrument] = context.instrument_lookup.find_by_symbol(target.symbol)
target_token: typing.Optional[
mango.Instrument
] = context.instrument_lookup.find_by_symbol(target.symbol)
if target_token is None:
raise Exception(f"Could not find target token '{target.symbol}.")
market_symbol: str = f"serum:{target_token.symbol}/{quote_token.symbol}"
@ -67,7 +95,9 @@ for target in targets:
prices += [mango.InstrumentValue(target_token, price.mid_price)]
prices += [mango.InstrumentValue(quote_token, Decimal(1))]
wallet_balancer = mango.LiveWalletBalancer(wallet, quote_token, trade_executor, targets, action_threshold)
wallet_balancer = mango.LiveWalletBalancer(
wallet, quote_token, trade_executor, targets, action_threshold
)
wallet_balancer.balance(context, prices)
logging.info("Balancing completed.")

View File

@ -7,40 +7,60 @@ import sys
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Cancels all orders on a market from the current wallet.")
parser = argparse.ArgumentParser(
description="Cancels all orders on a market from the current wallet."
)
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--market", type=str, required=True, help="market symbol where orders are placed (e.g. ETH/USDC)")
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(
"--market",
type=str,
required=True,
help="market symbol where orders are placed (e.g. ETH/USDC)",
)
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",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
group = mango.Group.load(context, context.group_address)
account = mango.Account.load_for_owner_by_address(context, wallet.address, group, args.account_address)
account = mango.Account.load_for_owner_by_address(
context, wallet.address, group, args.account_address
)
market = context.market_lookup.find_by_symbol(args.market)
if market is None:
raise Exception(f"Could not find market {args.market}")
market_operations = mango.create_market_operations(context, wallet, account, market, args.dry_run)
market_operations = mango.create_market_operations(
context, wallet, account, market, args.dry_run
)
orders = market_operations.load_my_orders()
if len(orders) == 0:
mango.output(f"No open orders on {market.symbol}")
else:
if isinstance(market_operations.market, mango.PerpMarket):
instruction_builder = mango.PerpMarketInstructionBuilder(
context, wallet, market_operations.market, group, account)
context, wallet, market_operations.market, group, account
)
cancel_all = instruction_builder.build_cancel_all_orders_instructions()
signers: mango.CombinableInstructions = mango.CombinableInstructions.from_wallet(wallet)
signers: mango.CombinableInstructions = (
mango.CombinableInstructions.from_wallet(wallet)
)
cancel_all_signatures = (signers + cancel_all).execute(context)
mango.output(f"Cancelling all perp orders: {cancel_all_signatures}")
else:

View File

@ -7,38 +7,65 @@ import sys
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Shows all orders on a market.")
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--market", type=str, required=True, help="market symbol to use (e.g. ETH/USDC)")
parser.add_argument("--id", type=int,
help="order ID of the order to cancel (either --client-id must be specified, or both --id and --side must be specified")
parser.add_argument("--client-id", type=int,
help="client ID of the order to cancel (either --client-id must be specified, or both --id and --side must be specified")
parser.add_argument("--side", type=mango.Side, default=mango.Side.BUY, choices=list(mango.Side),
help="whether the order to cancel is a BUY or a SELL (either --client-id must be specified, or both --id and --side must be specified")
parser.add_argument("--account-address", type=PublicKey,
help="address of the specific account to use, if more than one available")
parser.add_argument("--ok-if-missing", action="store_true", default=False,
help="if supported by market type (PERP-only for now) will not error if the ID does not exist")
parser.add_argument("--dry-run", action="store_true", default=False,
help="runs as read-only and does not perform any transactions")
parser.add_argument(
"--market", type=str, required=True, help="market symbol to use (e.g. ETH/USDC)"
)
parser.add_argument(
"--id",
type=int,
help="order ID of the order to cancel (either --client-id must be specified, or both --id and --side must be specified",
)
parser.add_argument(
"--client-id",
type=int,
help="client ID of the order to cancel (either --client-id must be specified, or both --id and --side must be specified",
)
parser.add_argument(
"--side",
type=mango.Side,
default=mango.Side.BUY,
choices=list(mango.Side),
help="whether the order to cancel is a BUY or a SELL (either --client-id must be specified, or both --id and --side must be specified",
)
parser.add_argument(
"--account-address",
type=PublicKey,
help="address of the specific account to use, if more than one available",
)
parser.add_argument(
"--ok-if-missing",
action="store_true",
default=False,
help="if supported by market type (PERP-only for now) will not error if the ID does not exist",
)
parser.add_argument(
"--dry-run",
action="store_true",
default=False,
help="runs as read-only and does not perform any transactions",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
group = mango.Group.load(context, context.group_address)
account = mango.Account.load_for_owner_by_address(context, wallet.address, group, args.account_address)
account = mango.Account.load_for_owner_by_address(
context, wallet.address, group, args.account_address
)
market = context.market_lookup.find_by_symbol(args.market)
if market is None:
raise Exception(f"Could not find market {args.market}")
market_operations = mango.create_market_operations(context, wallet, account, market, args.dry_run)
market_operations = mango.create_market_operations(
context, wallet, account, market, args.dry_run
)
order = mango.Order.from_ids(id=args.id, client_id=args.client_id, side=args.side)
cancellation = market_operations.cancel_order(order, ok_if_missing=args.ok_if_missing)

View File

@ -7,32 +7,40 @@ import typing
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Closes a Wrapped SOL account.")
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--address", type=PublicKey,
help="Public key of the Wrapped SOL account to close")
parser.add_argument(
"--address", type=PublicKey, help="Public key of the Wrapped SOL account to close"
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
wrapped_sol: mango.Token = mango.Token.ensure(context.instrument_lookup.find_by_symbol_or_raise("SOL"))
wrapped_sol: mango.Token = mango.Token.ensure(
context.instrument_lookup.find_by_symbol_or_raise("SOL")
)
token_account: typing.Optional[mango.TokenAccount] = mango.TokenAccount.load(context, args.address)
token_account: typing.Optional[mango.TokenAccount] = mango.TokenAccount.load(
context, args.address
)
if (token_account is None) or (token_account.value.token != wrapped_sol):
raise Exception(f"Account {args.address} is not a {wrapped_sol.name} account.")
payer: PublicKey = wallet.address
signers: mango.CombinableInstructions = mango.CombinableInstructions.from_wallet(wallet)
close_instruction = mango.build_close_spl_account_instructions(context, wallet, args.address)
close_instruction = mango.build_close_spl_account_instructions(
context, wallet, args.address
)
mango.output(f"Closing account: {args.address} with balance {token_account.value.value} lamports.")
mango.output(
f"Closing account: {args.address} with balance {token_account.value.value} lamports."
)
all_instructions = signers + close_instruction
transaction_ids = all_instructions.execute(context)

View File

@ -9,25 +9,40 @@ import sys
from decimal import Decimal
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), '..')))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Cranks all openorders in the market.")
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--market", type=str, required=True, help="market symbol to crank (e.g. ETH/USDC)")
parser.add_argument("--limit", type=Decimal, default=Decimal(32), help="maximum number of events to be processed")
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(
"--market", type=str, required=True, help="market symbol to crank (e.g. ETH/USDC)"
)
parser.add_argument(
"--limit",
type=Decimal,
default=Decimal(32),
help="maximum number of events to be processed",
)
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",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
group = mango.Group.load(context, context.group_address)
account = mango.Account.load_for_owner_by_address(context, wallet.address, group, args.account_address)
account = mango.Account.load_for_owner_by_address(
context, wallet.address, group, args.account_address
)
logging.info(f"Wallet address: {wallet.address}")
@ -35,7 +50,9 @@ market = context.market_lookup.find_by_symbol(args.market)
if market is None:
raise Exception(f"Could not find market {args.market}")
market_operations = mango.create_market_operations(context, wallet, account, market, args.dry_run)
market_operations = mango.create_market_operations(
context, wallet, account, market, args.dry_run
)
crank = market_operations.crank(args.limit)
mango.output(crank)

View File

@ -7,19 +7,32 @@ import sys
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Delegate operational authority of a Mango account to another account.")
parser = argparse.ArgumentParser(
description="Delegate operational authority of a Mango account to another account."
)
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--account-address", type=PublicKey, required=True,
help="address of the Mango account to delegate")
parser.add_argument("--delegate-address", type=PublicKey, required=False,
help="address of the account to which to delegate operational control of the Mango account")
parser.add_argument("--revoke", action="store_true", default=False,
help="revoke any previous account delegation")
parser.add_argument(
"--account-address",
type=PublicKey,
required=True,
help="address of the Mango account to delegate",
)
parser.add_argument(
"--delegate-address",
type=PublicKey,
required=False,
help="address of the account to which to delegate operational control of the Mango account",
)
parser.add_argument(
"--revoke",
action="store_true",
default=False,
help="revoke any previous account delegation",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
@ -29,18 +42,25 @@ wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
group = mango.Group.load(context, context.group_address)
account: mango.Account = mango.Account.load(context, args.account_address, group)
if account.owner != wallet.address:
raise Exception(f"Account {account.address} is not owned by current wallet {wallet.address}.")
raise Exception(
f"Account {account.address} is not owned by current wallet {wallet.address}."
)
all_instructions: mango.CombinableInstructions = mango.CombinableInstructions.from_signers([wallet.keypair])
all_instructions: mango.CombinableInstructions = (
mango.CombinableInstructions.from_signers([wallet.keypair])
)
if args.revoke:
unset_delegate_instructions = mango.build_unset_account_delegate_instructions(context, wallet, group, account)
unset_delegate_instructions = mango.build_unset_account_delegate_instructions(
context, wallet, group, account
)
all_instructions += unset_delegate_instructions
else:
if args.delegate_address is None:
raise Exception("No delegate address specified")
set_delegate_instructions = mango.build_set_account_delegate_instructions(
context, wallet, group, account, args.delegate_address)
context, wallet, group, account, args.delegate_address
)
all_instructions += set_delegate_instructions
transaction_ids = all_instructions.execute(context)

View File

@ -8,37 +8,53 @@ import sys
from decimal import Decimal
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), '..')))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="deposit funds into a Mango account")
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--symbol", type=str, required=True, help="token symbol to deposit (e.g. USDC)")
parser.add_argument("--quantity", type=Decimal, required=True, help="quantity token to deposit")
parser.add_argument("--account-address", type=PublicKey,
help="address of the specific account to use, if more than one available")
parser.add_argument(
"--symbol", type=str, required=True, help="token symbol to deposit (e.g. USDC)"
)
parser.add_argument(
"--quantity", type=Decimal, required=True, help="quantity token to deposit"
)
parser.add_argument(
"--account-address",
type=PublicKey,
help="address of the specific account to use, if more than one available",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
group = mango.Group.load(context, context.group_address)
account = mango.Account.load_for_owner_by_address(context, wallet.address, group, args.account_address)
account = mango.Account.load_for_owner_by_address(
context, wallet.address, group, args.account_address
)
instrument = context.instrument_lookup.find_by_symbol(args.symbol)
if instrument is None:
raise Exception(f"Could not find instrument with symbol '{args.symbol}'.")
token: mango.Token = mango.Token.ensure(instrument)
token_account = mango.TokenAccount.fetch_largest_for_owner_and_token(context, wallet.keypair.public_key, token)
token_account = mango.TokenAccount.fetch_largest_for_owner_and_token(
context, wallet.keypair.public_key, token
)
if token_account is None:
raise Exception(f"Could not find token account for token {token} with owner {wallet.keypair}.")
raise Exception(
f"Could not find token account for token {token} with owner {wallet.keypair}."
)
deposit_value = mango.InstrumentValue(token, args.quantity)
deposit_token_account = mango.TokenAccount(
token_account.account_info, token_account.version, token_account.owner, deposit_value)
token_account.account_info,
token_account.version,
token_account.owner,
deposit_value,
)
token_bank = group.token_bank_by_instrument(token)
root_bank = token_bank.ensure_root_bank(context)
@ -46,7 +62,8 @@ node_bank = root_bank.pick_node_bank(context)
signers: mango.CombinableInstructions = mango.CombinableInstructions.from_wallet(wallet)
deposit = mango.build_deposit_instructions(
context, wallet, group, account, root_bank, node_bank, deposit_token_account)
context, wallet, group, account, root_bank, node_bank, deposit_token_account
)
all_instructions = signers + deposit
transaction_ids = all_instructions.execute(context)

View File

@ -10,20 +10,31 @@ from datetime import datetime, timedelta, timezone
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(
description="Downloads perp and spot trades for the current wallet. Will perform incremental updates to the given filename instead of re-downloading all trades.")
description="Downloads perp and spot trades for the current wallet. Will perform incremental updates to the given filename instead of re-downloading all trades."
)
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--address", type=PublicKey, required=False,
help="Address of the Mango account to load (defaults to current wallet's Mango account)")
parser.add_argument("--filename", type=str, required=False,
help="filename for loading and storing the trade history data in CSV format")
parser.add_argument("--most-recent-hours", type=int,
help="only retrieve and save the most recent number of hours (e.g. --most-recent-hours 24)")
parser.add_argument(
"--address",
type=PublicKey,
required=False,
help="Address of the Mango account to load (defaults to current wallet's Mango account)",
)
parser.add_argument(
"--filename",
type=str,
required=False,
help="filename for loading and storing the trade history data in CSV format",
)
parser.add_argument(
"--most-recent-hours",
type=int,
help="only retrieve and save the most recent number of hours (e.g. --most-recent-hours 24)",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)

View File

@ -5,15 +5,20 @@ import os
import os.path
import sys
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), '..')))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Ensure a Mango account exists for the wallet and group.")
parser = argparse.ArgumentParser(
description="Ensure a Mango account exists for the wallet and group."
)
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--wait", action="store_true", default=False,
help="wait until the transaction is confirmed")
parser.add_argument(
"--wait",
action="store_true",
default=False,
help="wait until the transaction is confirmed",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
@ -23,9 +28,13 @@ group = mango.Group.load(context)
accounts = mango.Account.load_all_for_owner(context, wallet.address, group)
if len(accounts) > 0:
mango.output(f"At least one account already exists for group {group.address} and wallet {wallet.address}")
mango.output(
f"At least one account already exists for group {group.address} and wallet {wallet.address}"
)
else:
signers: mango.CombinableInstructions = mango.CombinableInstructions.from_wallet(wallet)
signers: mango.CombinableInstructions = mango.CombinableInstructions.from_wallet(
wallet
)
init = mango.build_create_account_instructions(context, wallet, group)
all_instructions = signers + init
transaction_ids = all_instructions.execute(context)

View File

@ -8,15 +8,18 @@ import typing
import spl.token.instructions as spl_token
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), '..')))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="mint SPL tokens to your wallet")
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--symbol", type=str, required=True,
help="token symbol to ensure the associated token account exists (e.g. USDC)")
parser.add_argument(
"--symbol",
type=str,
required=True,
help="token symbol to ensure the associated token account exists (e.g. USDC)",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
@ -27,18 +30,28 @@ if instrument is None:
raise Exception(f"Could not find instrument with symbol '{args.symbol}'.")
token: mango.Token = mango.Token.ensure(instrument)
associated_token_address = spl_token.get_associated_token_address(wallet.address, token.mint)
token_account: typing.Optional[mango.TokenAccount] = mango.TokenAccount.load(context, associated_token_address)
associated_token_address = spl_token.get_associated_token_address(
wallet.address, token.mint
)
token_account: typing.Optional[mango.TokenAccount] = mango.TokenAccount.load(
context, associated_token_address
)
if token_account is not None:
# The associated token account exists
mango.output(f"Associated token account already exists at: {associated_token_address}.")
mango.output(
f"Associated token account already exists at: {associated_token_address}."
)
else:
# Create the proper associated token account.
signer = mango.CombinableInstructions.from_wallet(wallet)
create_instruction = spl_token.create_associated_token_account(wallet.address, wallet.address, token.mint)
create_instruction = spl_token.create_associated_token_account(
wallet.address, wallet.address, token.mint
)
create = mango.CombinableInstructions.from_instruction(create_instruction)
mango.output(f"No associated token account at: {associated_token_address} - creating...")
mango.output(
f"No associated token account at: {associated_token_address} - creating..."
)
transaction_ids = (signer + create).execute(context)
context.client.wait_for_confirmation(transaction_ids)
mango.output(f"Associated token account created at: {associated_token_address}.")

View File

@ -7,31 +7,45 @@ import sys
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), '..')))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Ensure an OpenOrders account exists for the wallet and market.")
parser = argparse.ArgumentParser(
description="Ensure an OpenOrders account exists for the wallet and market."
)
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--market", type=str, required=True, help="market symbol to buy (e.g. ETH/USDC)")
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(
"--market", type=str, required=True, help="market symbol to buy (e.g. ETH/USDC)"
)
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",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
group = mango.Group.load(context)
account = mango.Account.load_for_owner_by_address(context, wallet.address, group, args.account_address)
account = mango.Account.load_for_owner_by_address(
context, wallet.address, group, args.account_address
)
market = context.market_lookup.find_by_symbol(args.market)
if market is None:
raise Exception(f"Could not find market {args.market}")
loaded_market: mango.Market = mango.ensure_market_loaded(context, market)
market_operations = mango.create_market_operations(context, wallet, account, market, args.dry_run)
market_operations = mango.create_market_operations(
context, wallet, account, market, args.dry_run
)
open_orders = market_operations.ensure_openorders()
mango.output(f"OpenOrders account for {market.symbol} is {open_orders}")

View File

@ -5,19 +5,30 @@ import os
import os.path
import sys
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), '..')))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="generates a Solana keypair and writes it to an ID file")
parser = argparse.ArgumentParser(
description="generates a Solana keypair and writes it to an ID file"
)
mango.ContextBuilder.add_command_line_parameters(parser)
parser.add_argument("--filename", type=str, default="id.json",
help="filename for saving the JSON-formatted keypair (default: id.json)")
parser.add_argument("--overwrite", action="store_true", default=False, help="overwrite the file if it exists")
parser.add_argument(
"--filename",
type=str,
default="id.json",
help="filename for saving the JSON-formatted keypair (default: id.json)",
)
parser.add_argument(
"--overwrite",
action="store_true",
default=False,
help="overwrite the file if it exists",
)
args: argparse.Namespace = mango.parse_args(parser)
if os.path.isdir(args.filename):
mango.output(f"""ERROR: Filename parameter {args.filename} is a directory, not a file.
mango.output(
f"""ERROR: Filename parameter {args.filename} is a directory, not a file.
This can happen when docker auto-creates -v parameters if they don't already exist. To work around this problem, the file {args.filename} must exist before the first time the docker container is run.
@ -26,12 +37,15 @@ If you are running this command via docker, and this error is unexpected, run th
touch '{args.filename}'
chmod 600 '{args.filename}'
Then run your generate-keypair command again.""")
Then run your generate-keypair command again."""
)
else:
wallet = mango.Wallet.create()
wallet.save(args.filename, args.overwrite)
mango.output(f"""
mango.output(
f"""
Wrote new keypair to {args.filename}
==================================================================================
pubkey: {wallet.address}
==================================================================================""")
=================================================================================="""
)

View File

@ -5,14 +5,18 @@ import os
import os.path
import sys
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), '..')))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Initializes a Mango margin account")
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--wait", action="store_true", default=False, help="wait until the transaction is confirmed")
parser.add_argument(
"--wait",
action="store_true",
default=False,
help="wait until the transaction is confirmed",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)

View File

@ -10,8 +10,7 @@ import traceback
from decimal import Decimal
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
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
@ -19,21 +18,50 @@ import mango # nopep8
parser = argparse.ArgumentParser(description="Liquidate a single margin account.")
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--address", type=PublicKey,
help="Solana address of the Mango Markets margin account to be liquidated")
parser.add_argument("--notify-liquidations", type=mango.parse_notification_target, action="append", default=[],
help="The notification target for liquidation events")
parser.add_argument("--notify-successful-liquidations", type=mango.parse_notification_target,
action="append", default=[], help="The notification target for successful liquidation events")
parser.add_argument("--notify-failed-liquidations", type=mango.parse_notification_target,
action="append", default=[], help="The notification target for failed liquidation events")
parser.add_argument("--notify-errors", type=mango.parse_notification_target, action="append", default=[],
help="The notification target for error events")
parser.add_argument("--dry-run", action="store_true", default=False,
help="runs as read-only and does not perform any transactions")
parser.add_argument(
"--address",
type=PublicKey,
help="Solana address of the Mango Markets margin account to be liquidated",
)
parser.add_argument(
"--notify-liquidations",
type=mango.parse_notification_target,
action="append",
default=[],
help="The notification target for liquidation events",
)
parser.add_argument(
"--notify-successful-liquidations",
type=mango.parse_notification_target,
action="append",
default=[],
help="The notification target for successful liquidation events",
)
parser.add_argument(
"--notify-failed-liquidations",
type=mango.parse_notification_target,
action="append",
default=[],
help="The notification target for failed liquidation events",
)
parser.add_argument(
"--notify-errors",
type=mango.parse_notification_target,
action="append",
default=[],
help="The notification target for error events",
)
parser.add_argument(
"--dry-run",
action="store_true",
default=False,
help="runs as read-only and does not perform any transactions",
)
args: argparse.Namespace = mango.parse_args(parser)
handler = mango.NotificationHandler(mango.CompoundNotificationTarget(args.notify_errors))
handler = mango.NotificationHandler(
mango.CompoundNotificationTarget(args.notify_errors)
)
handler.setLevel(logging.ERROR)
logging.getLogger().addHandler(handler)
@ -53,22 +81,27 @@ try:
report = scout.verify_account_prepared_for_group(context, group, wallet.address)
logging.info(f"Wallet account report: {report}")
if report.has_errors:
raise Exception(f"Account '{wallet.address}' is not prepared for group '{group.address}'.")
raise Exception(
f"Account '{wallet.address}' is not prepared for group '{group.address}'."
)
logging.info("Wallet accounts OK.")
liquidations_publisher = mango.EventSource[mango.LiquidationEvent]()
liquidations_publisher.subscribe(on_next=mango.CompoundNotificationTarget(
args.notify_liquidations).send) # type: ignore[call-arg]
liquidations_publisher.subscribe(
on_next=mango.CompoundNotificationTarget(args.notify_liquidations).send
) # type: ignore[call-arg]
on_success = mango.FilteringNotificationTarget(
mango.CompoundNotificationTarget(args.notify_successful_liquidations),
lambda item: isinstance(item, mango.LiquidationEvent) and item.succeeded)
lambda item: isinstance(item, mango.LiquidationEvent) and item.succeeded,
)
liquidations_publisher.subscribe(on_next=on_success.send) # type: ignore[call-arg]
on_failed = mango.FilteringNotificationTarget(
mango.CompoundNotificationTarget(args.notify_failed_liquidations),
lambda item: isinstance(item, mango.LiquidationEvent) and not item.succeeded)
lambda item: isinstance(item, mango.LiquidationEvent) and not item.succeeded,
)
liquidations_publisher.subscribe(on_next=on_failed.send) # type: ignore[call-arg]
# TODO: Add proper liquidator classes here when they're written for V3
@ -81,7 +114,9 @@ try:
# prices = group.fetch_token_prices(context)
account = mango.Account.load(context, account_address, group)
worthwhile_threshold = Decimal(0) # No threshold - don't take this into account.
liquidatable_report = mango.LiquidatableReport.build(group, [], account, worthwhile_threshold)
liquidatable_report = mango.LiquidatableReport.build(
group, [], account, worthwhile_threshold
)
transaction_ids = account_liquidator.liquidate(liquidatable_report)
if transaction_ids is None or len(transaction_ids) == 0:
mango.output("No transaction sent.")
@ -96,8 +131,12 @@ try:
mango.output(str(transaction_scout))
except Exception as exception:
logging.critical(f"Liquidator stopped because of exception: {exception} - {traceback.format_exc()}")
logging.critical(
f"Liquidator stopped because of exception: {exception} - {traceback.format_exc()}"
)
except:
logging.critical(f"Liquidator stopped because of uncatchable error: {traceback.format_exc()}")
logging.critical(
f"Liquidator stopped because of uncatchable error: {traceback.format_exc()}"
)
finally:
logging.info("Liquidation complete.")

View File

@ -13,63 +13,141 @@ import typing
from decimal import Decimal
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
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="Run a liquidator for a Mango Markets group.")
parser = argparse.ArgumentParser(
description="Run a liquidator for a Mango Markets group."
)
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--throttle-reload-to-seconds", type=Decimal, default=Decimal(60),
help="minimum number of seconds between each full margin account reload loop (including time taken processing accounts)")
parser.add_argument("--throttle-ripe-update-to-seconds", type=Decimal, default=Decimal(5),
help="minimum number of seconds between each ripe update loop (including time taken processing accounts)")
parser.add_argument("--target", type=mango.parse_target_balance, action="append",
help="token symbol plus target value or percentage, separated by a colon (e.g. 'ETH:2.5')")
parser.add_argument("--action-threshold", type=Decimal, default=Decimal("0.01"),
help="fraction of total wallet value a trade must be above to be carried out")
parser.add_argument("--worthwhile-threshold", type=Decimal, default=Decimal("0.01"),
help="value a liquidation must be above to be carried out")
parser.add_argument("--adjustment-factor", type=Decimal, default=Decimal("0.05"),
help="factor by which to adjust the SELL price (akin to maximum slippage)")
parser.add_argument("--notify-liquidations", type=mango.parse_notification_target, action="append", default=[],
help="The notification target for liquidation events")
parser.add_argument("--notify-successful-liquidations", type=mango.parse_notification_target,
action="append", default=[], help="The notification target for successful liquidation events")
parser.add_argument("--notify-failed-liquidations", type=mango.parse_notification_target,
action="append", default=[], help="The notification target for failed liquidation events")
parser.add_argument("--notify-errors", type=mango.parse_notification_target, action="append", default=[],
help="The notification target for error events")
parser.add_argument("--dry-run", action="store_true", default=False,
help="runs as read-only and does not perform any transactions")
parser.add_argument(
"--throttle-reload-to-seconds",
type=Decimal,
default=Decimal(60),
help="minimum number of seconds between each full margin account reload loop (including time taken processing accounts)",
)
parser.add_argument(
"--throttle-ripe-update-to-seconds",
type=Decimal,
default=Decimal(5),
help="minimum number of seconds between each ripe update loop (including time taken processing accounts)",
)
parser.add_argument(
"--target",
type=mango.parse_target_balance,
action="append",
help="token symbol plus target value or percentage, separated by a colon (e.g. 'ETH:2.5')",
)
parser.add_argument(
"--action-threshold",
type=Decimal,
default=Decimal("0.01"),
help="fraction of total wallet value a trade must be above to be carried out",
)
parser.add_argument(
"--worthwhile-threshold",
type=Decimal,
default=Decimal("0.01"),
help="value a liquidation must be above to be carried out",
)
parser.add_argument(
"--adjustment-factor",
type=Decimal,
default=Decimal("0.05"),
help="factor by which to adjust the SELL price (akin to maximum slippage)",
)
parser.add_argument(
"--notify-liquidations",
type=mango.parse_notification_target,
action="append",
default=[],
help="The notification target for liquidation events",
)
parser.add_argument(
"--notify-successful-liquidations",
type=mango.parse_notification_target,
action="append",
default=[],
help="The notification target for successful liquidation events",
)
parser.add_argument(
"--notify-failed-liquidations",
type=mango.parse_notification_target,
action="append",
default=[],
help="The notification target for failed liquidation events",
)
parser.add_argument(
"--notify-errors",
type=mango.parse_notification_target,
action="append",
default=[],
help="The notification target for error events",
)
parser.add_argument(
"--dry-run",
action="store_true",
default=False,
help="runs as read-only and does not perform any transactions",
)
args: argparse.Namespace = mango.parse_args(parser)
handler = mango.NotificationHandler(mango.CompoundNotificationTarget(args.notify_errors))
handler = mango.NotificationHandler(
mango.CompoundNotificationTarget(args.notify_errors)
)
handler.setLevel(logging.ERROR)
logging.getLogger().addHandler(handler)
def start_subscriptions(context: mango.Context, liquidation_processor: mango.LiquidationProcessor, fetch_prices: typing.Callable[[typing.Any], typing.Any], fetch_accounts: typing.Callable[[typing.Any], typing.Any], throttle_reload_to_seconds: Decimal, throttle_ripe_update_to_seconds: Decimal) -> typing.Tuple[rx.core.typing.Disposable, rx.core.typing.Disposable]:
def start_subscriptions(
context: mango.Context,
liquidation_processor: mango.LiquidationProcessor,
fetch_prices: typing.Callable[[typing.Any], typing.Any],
fetch_accounts: typing.Callable[[typing.Any], typing.Any],
throttle_reload_to_seconds: Decimal,
throttle_ripe_update_to_seconds: Decimal,
) -> typing.Tuple[rx.core.typing.Disposable, rx.core.typing.Disposable]:
liquidation_processor.state = mango.LiquidationProcessorState.STARTING
logging.info("Starting margin account fetcher subscription")
account_subscription = rx.interval(float(throttle_reload_to_seconds)).pipe(
ops.observe_on(context.create_thread_pool_scheduler()),
ops.start_with(-1),
ops.map(fetch_accounts(context)),
ops.catch(mango.observable_pipeline_error_reporter),
ops.retry()
).subscribe(mango.create_backpressure_skipping_observer(on_next=liquidation_processor.update_accounts, on_error=mango.log_subscription_error))
account_subscription = (
rx.interval(float(throttle_reload_to_seconds))
.pipe(
ops.observe_on(context.create_thread_pool_scheduler()),
ops.start_with(-1),
ops.map(fetch_accounts(context)),
ops.catch(mango.observable_pipeline_error_reporter),
ops.retry(),
)
.subscribe(
mango.create_backpressure_skipping_observer(
on_next=liquidation_processor.update_accounts,
on_error=mango.log_subscription_error,
)
)
)
logging.info("Starting price fetcher subscription")
price_subscription = rx.interval(float(throttle_ripe_update_to_seconds)).pipe(
ops.observe_on(context.create_thread_pool_scheduler()),
ops.map(fetch_prices(context)),
ops.catch(mango.observable_pipeline_error_reporter),
ops.retry()
).subscribe(mango.create_backpressure_skipping_observer(on_next=lambda piped: liquidation_processor.update_prices(piped[0], piped[1]), on_error=mango.log_subscription_error))
price_subscription = (
rx.interval(float(throttle_ripe_update_to_seconds))
.pipe(
ops.observe_on(context.create_thread_pool_scheduler()),
ops.map(fetch_prices(context)),
ops.catch(mango.observable_pipeline_error_reporter),
ops.retry(),
)
.subscribe(
mango.create_backpressure_skipping_observer(
on_next=lambda piped: liquidation_processor.update_prices(
piped[0], piped[1]
),
on_error=mango.log_subscription_error,
)
)
)
return account_subscription, price_subscription
@ -95,22 +173,27 @@ try:
report = scout.verify_account_prepared_for_group(context, group, wallet.address)
logging.info(f"Wallet account report: {report}")
if report.has_errors:
raise Exception(f"Account '{wallet.address}' is not prepared for group '{group.address}'.")
raise Exception(
f"Account '{wallet.address}' is not prepared for group '{group.address}'."
)
logging.info("Wallet accounts OK.")
liquidations_publisher = mango.EventSource[mango.LiquidationEvent]()
liquidations_publisher.subscribe(on_next=mango.CompoundNotificationTarget(
args.notify_liquidations).send) # type: ignore[call-arg]
liquidations_publisher.subscribe(
on_next=mango.CompoundNotificationTarget(args.notify_liquidations).send
) # type: ignore[call-arg]
on_success = mango.FilteringNotificationTarget(
mango.CompoundNotificationTarget(args.notify_successful_liquidations),
lambda item: isinstance(item, mango.LiquidationEvent) and item.succeeded)
lambda item: isinstance(item, mango.LiquidationEvent) and item.succeeded,
)
liquidations_publisher.subscribe(on_next=on_success.send) # type: ignore[call-arg]
on_failed = mango.FilteringNotificationTarget(
mango.CompoundNotificationTarget(args.notify_failed_liquidations),
lambda item: isinstance(item, mango.LiquidationEvent) and not item.succeeded)
lambda item: isinstance(item, mango.LiquidationEvent) and not item.succeeded,
)
liquidations_publisher.subscribe(on_next=on_failed.send) # type: ignore[call-arg]
# TODO: Add proper liquidator classes here when they're written for V3
@ -123,16 +206,25 @@ try:
wallet_balancer: mango.WalletBalancer = mango.NullWalletBalancer()
else:
targets = args.target
trade_executor = mango.ImmediateTradeExecutor(context, wallet, None, adjustment_factor)
trade_executor = mango.ImmediateTradeExecutor(
context, wallet, None, adjustment_factor
)
wallet_balancer = mango.LiveWalletBalancer(
wallet, group.shared_quote_token, trade_executor, targets, action_threshold)
wallet, group.shared_quote_token, trade_executor, targets, action_threshold
)
# These (along with `context`) are captured and read by `load_updated_price_details()`.
group_address = group.address
oracle_addresses = group.oracles
def load_updated_price_details() -> typing.Tuple[mango.Group, typing.Sequence[mango.InstrumentValue]]:
oracles = [oracle_address for oracle_address in oracle_addresses if oracle_address is not None]
def load_updated_price_details() -> typing.Tuple[
mango.Group, typing.Sequence[mango.InstrumentValue]
]:
oracles = [
oracle_address
for oracle_address in oracle_addresses
if oracle_address is not None
]
all_addresses = [group_address, *oracles]
all_account_infos = mango.AccountInfo.load_multiple(context, all_addresses)
group_account_info = all_account_infos[0]
@ -141,59 +233,93 @@ try:
# TODO - fetch prices when code available in V3.
return group, []
def fetch_prices(context: mango.Context) -> typing.Callable[[typing.Any], typing.Any]:
def fetch_prices(
context: mango.Context,
) -> typing.Callable[[typing.Any], typing.Any]:
def _fetch_prices(_: typing.Any) -> typing.Any:
with mango.retry_context("Price Fetch",
lambda _: load_updated_price_details(),
context.retry_pauses) as retrier:
with mango.retry_context(
"Price Fetch",
lambda _: load_updated_price_details(),
context.retry_pauses,
) as retrier:
return retrier.run()
return _fetch_prices
def fetch_accounts(context: mango.Context) -> typing.Callable[[typing.Any], typing.Any]:
def fetch_accounts(
context: mango.Context,
) -> typing.Callable[[typing.Any], typing.Any]:
def _actual_fetch() -> typing.Sequence[mango.Account]:
# group = mango.Group.load(context)
# return mango.Account.load_ripe(context, group)
return []
def _fetch_accounts(_: typing.Any) -> typing.Any:
with mango.retry_context("Margin Account Fetch",
lambda _: _actual_fetch(),
context.retry_pauses) as retrier:
with mango.retry_context(
"Margin Account Fetch", lambda _: _actual_fetch(), context.retry_pauses
) as retrier:
return retrier.run()
return _fetch_accounts
class LiquidationProcessorSubscriptions:
def __init__(self, account: rx.core.typing.Disposable, price: rx.core.typing.Disposable) -> None:
def __init__(
self, account: rx.core.typing.Disposable, price: rx.core.typing.Disposable
) -> None:
self.account: rx.core.typing.Disposable = account
self.price: rx.core.typing.Disposable = price
liquidation_processor = mango.LiquidationProcessor(
context, liquidator_name, account_liquidator, wallet_balancer, worthwhile_threshold)
context,
liquidator_name,
account_liquidator,
wallet_balancer,
worthwhile_threshold,
)
account_subscription, price_subscription = start_subscriptions(
context, liquidation_processor, fetch_prices, fetch_accounts, throttle_reload_to_seconds, throttle_ripe_update_to_seconds)
context,
liquidation_processor,
fetch_prices,
fetch_accounts,
throttle_reload_to_seconds,
throttle_ripe_update_to_seconds,
)
subscriptions = LiquidationProcessorSubscriptions(account=account_subscription,
price=price_subscription)
subscriptions = LiquidationProcessorSubscriptions(
account=account_subscription, price=price_subscription
)
def on_unhealthy(liquidation_processor: mango.LiquidationProcessor) -> None:
if liquidation_processor.state != mango.LiquidationProcessorState.UNHEALTHY:
logging.info(
f"Ignoring LiquidationProcessor state change - state is: {liquidation_processor.state}")
f"Ignoring LiquidationProcessor state change - state is: {liquidation_processor.state}"
)
return
logging.warning("Liquidation processor has been marked as unhealthy so recreating subscriptions.")
logging.warning(
"Liquidation processor has been marked as unhealthy so recreating subscriptions."
)
try:
subscriptions.account.dispose()
except Exception as exception:
logging.warning(f"Ignoring problem disposing of margin account subscription: {exception}")
logging.warning(
f"Ignoring problem disposing of margin account subscription: {exception}"
)
try:
subscriptions.price.dispose()
except Exception as exception:
logging.warning(f"Ignoring problem disposing of margin account subscription: {exception}")
logging.warning(
f"Ignoring problem disposing of margin account subscription: {exception}"
)
account_subscription, price_subscription = start_subscriptions(
context, liquidation_processor, fetch_prices, fetch_accounts, throttle_reload_to_seconds, throttle_ripe_update_to_seconds)
context,
liquidation_processor,
fetch_prices,
fetch_accounts,
throttle_reload_to_seconds,
throttle_ripe_update_to_seconds,
)
subscriptions.account = account_subscription
subscriptions.price = price_subscription
@ -205,8 +331,12 @@ try:
except KeyboardInterrupt:
logging.info("Liquidator stopping...")
except Exception as exception:
logging.critical(f"Liquidator stopped because of exception: {exception} - {traceback.format_exc()}")
logging.critical(
f"Liquidator stopped because of exception: {exception} - {traceback.format_exc()}"
)
except:
logging.critical(f"Liquidator stopped because of uncatchable error: {traceback.format_exc()}")
logging.critical(
f"Liquidator stopped because of uncatchable error: {traceback.format_exc()}"
)
finally:
logging.info("Liquidator completed.")

View File

@ -8,28 +8,55 @@ import sys
import time
import traceback
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
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="Run a single pass of the liquidator for a Mango Markets group.")
parser = argparse.ArgumentParser(
description="Run a single pass of the liquidator for a Mango Markets group."
)
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--notify-liquidations", type=mango.parse_notification_target, action="append", default=[],
help="The notification target for liquidation events")
parser.add_argument("--notify-successful-liquidations", type=mango.parse_notification_target,
action="append", default=[], help="The notification target for successful liquidation events")
parser.add_argument("--notify-failed-liquidations", type=mango.parse_notification_target,
action="append", default=[], help="The notification target for failed liquidation events")
parser.add_argument("--notify-errors", type=mango.parse_notification_target, action="append", default=[],
help="The notification target for error events")
parser.add_argument("--dry-run", action="store_true", default=False,
help="runs as read-only and does not perform any transactions")
parser.add_argument(
"--notify-liquidations",
type=mango.parse_notification_target,
action="append",
default=[],
help="The notification target for liquidation events",
)
parser.add_argument(
"--notify-successful-liquidations",
type=mango.parse_notification_target,
action="append",
default=[],
help="The notification target for successful liquidation events",
)
parser.add_argument(
"--notify-failed-liquidations",
type=mango.parse_notification_target,
action="append",
default=[],
help="The notification target for failed liquidation events",
)
parser.add_argument(
"--notify-errors",
type=mango.parse_notification_target,
action="append",
default=[],
help="The notification target for error events",
)
parser.add_argument(
"--dry-run",
action="store_true",
default=False,
help="runs as read-only and does not perform any transactions",
)
args: argparse.Namespace = mango.parse_args(parser)
handler = mango.NotificationHandler(mango.CompoundNotificationTarget(args.notify_errors))
handler = mango.NotificationHandler(
mango.CompoundNotificationTarget(args.notify_errors)
)
handler.setLevel(logging.ERROR)
logging.getLogger().addHandler(handler)
@ -48,22 +75,27 @@ try:
report = scout.verify_account_prepared_for_group(context, group, wallet.address)
logging.info(f"Wallet account report: {report}")
if report.has_errors:
raise Exception(f"Account '{wallet.address}' is not prepared for group '{group.address}'.")
raise Exception(
f"Account '{wallet.address}' is not prepared for group '{group.address}'."
)
logging.info("Wallet accounts OK.")
liquidations_publisher = mango.EventSource[mango.LiquidationEvent]()
liquidations_publisher.subscribe(on_next=mango.CompoundNotificationTarget(
args.notify_liquidations).send) # type: ignore[call-arg]
liquidations_publisher.subscribe(
on_next=mango.CompoundNotificationTarget(args.notify_liquidations).send
) # type: ignore[call-arg]
on_success = mango.FilteringNotificationTarget(
mango.CompoundNotificationTarget(args.notify_successful_liquidations),
lambda item: isinstance(item, mango.LiquidationEvent) and item.succeeded)
lambda item: isinstance(item, mango.LiquidationEvent) and item.succeeded,
)
liquidations_publisher.subscribe(on_next=on_success.send) # type: ignore[call-arg]
on_failed = mango.FilteringNotificationTarget(
mango.CompoundNotificationTarget(args.notify_failed_liquidations),
lambda item: isinstance(item, mango.LiquidationEvent) and not item.succeeded)
lambda item: isinstance(item, mango.LiquidationEvent) and not item.succeeded,
)
liquidations_publisher.subscribe(on_next=on_failed.send) # type: ignore[call-arg]
# TODO: Add proper liquidator classes here when they're written for V3
@ -74,7 +106,9 @@ try:
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()
liquidation_processor.update_accounts([])
@ -84,11 +118,17 @@ try:
liquidation_processor.update_prices(group, [])
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."
)
except Exception as exception:
logging.critical(f"Liquidator stopped because of exception: {exception} - {traceback.format_exc()}")
logging.critical(
f"Liquidator stopped because of exception: {exception} - {traceback.format_exc()}"
)
except:
logging.critical(f"Liquidator stopped because of uncatchable error: {traceback.format_exc()}")
logging.critical(
f"Liquidator stopped because of uncatchable error: {traceback.format_exc()}"
)
finally:
logging.info("Liquidator completed.")

View File

@ -9,14 +9,20 @@ import threading
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Show program logs for an account, as they arrive.")
parser = argparse.ArgumentParser(
description="Show program logs for an account, as they arrive."
)
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--address", type=PublicKey, required=True, help="Address of the Solana account to watch")
parser.add_argument(
"--address",
type=PublicKey,
required=True,
help="Address of the Solana account to watch",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)

View File

@ -3,8 +3,7 @@
import os
import sys
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
mango.output(mango.version())

View File

@ -14,71 +14,161 @@ import typing
from decimal import Decimal
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
import mango.hedging # nopep8
import mango.marketmaking # nopep8
from mango.marketmaking.orderchain import chain # nopep8
from mango.marketmaking.orderchain import chainbuilder # nopep8
parser = argparse.ArgumentParser(description="Runs a marketmaker against a particular market.")
parser = argparse.ArgumentParser(
description="Runs a marketmaker against a particular market."
)
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
chainbuilder.ChainBuilder.add_command_line_parameters(parser)
parser.add_argument("--market", type=str, required=True, help="market symbol to make market upon (e.g. ETH/USDC)")
parser.add_argument("--update-mode", type=mango.marketmaking.ModelUpdateMode, default=mango.marketmaking.ModelUpdateMode.POLL,
choices=list(mango.marketmaking.ModelUpdateMode), help="Update mode for model data - can be POLL (default) or WEBSOCKET")
parser.add_argument("--oracle-provider", type=str, required=True, help="name of the price provider to use (e.g. pyth)")
parser.add_argument("--oracle-market", type=str,
help="market symbol for oracle to use for pricing (e.g. ETH/USDC) - defaults to market specified in --market")
parser.add_argument("--order-type", type=mango.OrderType, default=mango.OrderType.POST_ONLY,
choices=list(mango.OrderType), help="Order type: LIMIT, IOC or POST_ONLY")
parser.add_argument("--existing-order-tolerance", type=Decimal, default=Decimal("0.001"),
help="tolerance in price and quantity when matching existing orders or cancelling/replacing")
parser.add_argument("--redeem-threshold", type=Decimal,
help="threshold above which liquidity incentives will be automatically moved to the account (default: no moving)")
parser.add_argument("--pulse-interval", type=float, default=10.0,
help="number of seconds between each 'pulse' of the market maker")
parser.add_argument("--hedging-pulse-interval", type=float,
help="number of seconds between each 'pulse' of the hedger (if hedging configured) - defaults to the --pulse-interval value if not specified")
parser.add_argument("--hedging-market", type=str, help="spot market symbol to use for hedging (e.g. ETH/USDC)")
parser.add_argument("--hedging-max-price-slippage-factor", type=Decimal, default=Decimal("0.05"),
help="the maximum value the IOC hedging order price can slip by when hedging (default is 0.05 for 5%%)")
parser.add_argument("--hedging-max-chunk-quantity", type=Decimal, default=Decimal(0),
help="the maximum quantity of the hedge asset that will be traded in a single pulse. Trades larger than this size will be 'chunked' and spread across subsequent hedge pulses.")
parser.add_argument("--hedging-target-balance", type=mango.parse_fixed_target_balance, required=False,
help="hedged balance to maintain - format is a token symbol plus target value, separated by a colon (e.g. 'ETH:2.5')")
parser.add_argument("--hedging-action-threshold", type=Decimal, default=Decimal(0),
help="minimum difference between spot and perp positions before action will be taken")
parser.add_argument("--hedging-pulse-pause-count", type=int, default=0,
help="number of pulses to pause after sending an order (to stop overtrading - a pause will prevent checking hedge delta and placing orders)")
parser.add_argument("--account-address", type=PublicKey,
help="address of the specific account to use, if more than one available")
parser.add_argument("--notify-errors", type=mango.parse_notification_target, action="append", default=[],
help="The notification target for error events")
parser.add_argument("--dry-run", action="store_true", default=False,
help="runs as read-only and does not perform any transactions")
parser.add_argument(
"--market",
type=str,
required=True,
help="market symbol to make market upon (e.g. ETH/USDC)",
)
parser.add_argument(
"--update-mode",
type=mango.marketmaking.ModelUpdateMode,
default=mango.marketmaking.ModelUpdateMode.POLL,
choices=list(mango.marketmaking.ModelUpdateMode),
help="Update mode for model data - can be POLL (default) or WEBSOCKET",
)
parser.add_argument(
"--oracle-provider",
type=str,
required=True,
help="name of the price provider to use (e.g. pyth)",
)
parser.add_argument(
"--oracle-market",
type=str,
help="market symbol for oracle to use for pricing (e.g. ETH/USDC) - defaults to market specified in --market",
)
parser.add_argument(
"--order-type",
type=mango.OrderType,
default=mango.OrderType.POST_ONLY,
choices=list(mango.OrderType),
help="Order type: LIMIT, IOC or POST_ONLY",
)
parser.add_argument(
"--existing-order-tolerance",
type=Decimal,
default=Decimal("0.001"),
help="tolerance in price and quantity when matching existing orders or cancelling/replacing",
)
parser.add_argument(
"--redeem-threshold",
type=Decimal,
help="threshold above which liquidity incentives will be automatically moved to the account (default: no moving)",
)
parser.add_argument(
"--pulse-interval",
type=float,
default=10.0,
help="number of seconds between each 'pulse' of the market maker",
)
parser.add_argument(
"--hedging-pulse-interval",
type=float,
help="number of seconds between each 'pulse' of the hedger (if hedging configured) - defaults to the --pulse-interval value if not specified",
)
parser.add_argument(
"--hedging-market",
type=str,
help="spot market symbol to use for hedging (e.g. ETH/USDC)",
)
parser.add_argument(
"--hedging-max-price-slippage-factor",
type=Decimal,
default=Decimal("0.05"),
help="the maximum value the IOC hedging order price can slip by when hedging (default is 0.05 for 5%%)",
)
parser.add_argument(
"--hedging-max-chunk-quantity",
type=Decimal,
default=Decimal(0),
help="the maximum quantity of the hedge asset that will be traded in a single pulse. Trades larger than this size will be 'chunked' and spread across subsequent hedge pulses.",
)
parser.add_argument(
"--hedging-target-balance",
type=mango.parse_fixed_target_balance,
required=False,
help="hedged balance to maintain - format is a token symbol plus target value, separated by a colon (e.g. 'ETH:2.5')",
)
parser.add_argument(
"--hedging-action-threshold",
type=Decimal,
default=Decimal(0),
help="minimum difference between spot and perp positions before action will be taken",
)
parser.add_argument(
"--hedging-pulse-pause-count",
type=int,
default=0,
help="number of pulses to pause after sending an order (to stop overtrading - a pause will prevent checking hedge delta and placing orders)",
)
parser.add_argument(
"--account-address",
type=PublicKey,
help="address of the specific account to use, if more than one available",
)
parser.add_argument(
"--notify-errors",
type=mango.parse_notification_target,
action="append",
default=[],
help="The notification target for error events",
)
parser.add_argument(
"--dry-run",
action="store_true",
default=False,
help="runs as read-only and does not perform any transactions",
)
args: argparse.Namespace = mango.parse_args(parser)
handler = mango.NotificationHandler(mango.CompoundNotificationTarget(args.notify_errors))
handler = mango.NotificationHandler(
mango.CompoundNotificationTarget(args.notify_errors)
)
handler.setLevel(logging.ERROR)
logging.getLogger().addHandler(handler)
def cleanup(context: mango.Context, wallet: mango.Wallet, account: mango.Account, market: mango.Market, dry_run: bool) -> None:
def cleanup(
context: mango.Context,
wallet: mango.Wallet,
account: mango.Account,
market: mango.Market,
dry_run: bool,
) -> None:
market_operations: mango.MarketOperations = mango.create_market_operations(
context, wallet, account, market, dry_run)
market_instruction_builder: mango.MarketInstructionBuilder = mango.create_market_instruction_builder(
context, wallet, account, market, dry_run)
context, wallet, account, market, dry_run
)
market_instruction_builder: mango.MarketInstructionBuilder = (
mango.create_market_instruction_builder(
context, wallet, account, market, dry_run
)
)
cancels: mango.CombinableInstructions = mango.CombinableInstructions.empty()
orders = market_operations.load_my_orders()
for order in orders:
cancels += market_instruction_builder.build_cancel_order_instructions(order, ok_if_missing=True)
cancels += market_instruction_builder.build_cancel_order_instructions(
order, ok_if_missing=True
)
if len(cancels.instructions) > 0:
logging.info(f"Cleaning up {len(cancels.instructions)} order(s).")
signer: mango.CombinableInstructions = mango.CombinableInstructions.from_wallet(wallet)
signer: mango.CombinableInstructions = mango.CombinableInstructions.from_wallet(
wallet
)
(signer + cancels).execute(context)
market_operations.crank()
market_operations.settle()
@ -94,14 +184,17 @@ disposer.add_disposable(health_check)
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
group = mango.Group.load(context, context.group_address)
account = mango.Account.load_for_owner_by_address(context, wallet.address, group, args.account_address)
account = mango.Account.load_for_owner_by_address(
context, wallet.address, group, args.account_address
)
market = mango.load_market_by_symbol(context, args.market)
# The market index is also the index of the base token in the group's token list.
if market.quote != group.shared_quote_token:
raise Exception(
f"Group {group.name} uses shared quote token {group.shared_quote_token.symbol}/{group.shared_quote_token.mint}, but market {market.symbol} uses quote token {market.quote.symbol}/{market.quote.mint}.")
f"Group {group.name} uses shared quote token {group.shared_quote_token.symbol}/{group.shared_quote_token.mint}, but market {market.symbol} uses quote token {market.quote.symbol}/{market.quote.mint}."
)
cleanup(context, wallet, account, market, args.dry_run)
@ -125,15 +218,25 @@ if args.hedging_market is not None:
logging.info(f"Hedging on {hedging_market.symbol}")
hedging_market_operations: mango.MarketOperations = mango.create_market_operations(
context, wallet, account, hedging_market, args.dry_run)
context, wallet, account, hedging_market, args.dry_run
)
target_balance: typing.Optional[mango.TargetBalance] = args.hedging_target_balance
if target_balance is None:
target_balance = mango.FixedTargetBalance(hedging_market.base.symbol, Decimal(0))
hedger = mango.hedging.PerpToSpotHedger(group, underlying_market, hedging_market,
hedging_market_operations, args.hedging_max_price_slippage_factor,
args.hedging_max_chunk_quantity, target_balance,
args.hedging_action_threshold, args.hedging_pulse_pause_count)
target_balance = mango.FixedTargetBalance(
hedging_market.base.symbol, Decimal(0)
)
hedger = mango.hedging.PerpToSpotHedger(
group,
underlying_market,
hedging_market,
hedging_market_operations,
args.hedging_max_price_slippage_factor,
args.hedging_max_chunk_quantity,
target_balance,
args.hedging_action_threshold,
args.hedging_pulse_pause_count,
)
order_reconciler: mango.marketmaking.OrderReconciler
@ -141,30 +244,63 @@ if args.existing_order_tolerance < 0:
order_reconciler = mango.marketmaking.AlwaysReplaceOrderReconciler()
else:
order_reconciler = mango.marketmaking.ToleranceOrderReconciler(
args.existing_order_tolerance, args.existing_order_tolerance)
args.existing_order_tolerance, args.existing_order_tolerance
)
desired_orders_chain: chain.Chain = chainbuilder.ChainBuilder.from_command_line_parameters(args)
desired_orders_chain: chain.Chain = (
chainbuilder.ChainBuilder.from_command_line_parameters(args)
)
logging.info(f"Desired orders chain: {desired_orders_chain}")
market_instruction_builder: mango.MarketInstructionBuilder = mango.create_market_instruction_builder(
context, wallet, account, market, args.dry_run)
market_instruction_builder: mango.MarketInstructionBuilder = (
mango.create_market_instruction_builder(
context, wallet, account, market, args.dry_run
)
)
market_maker = mango.marketmaking.MarketMaker(
wallet, market, market_instruction_builder, desired_orders_chain, order_reconciler, args.redeem_threshold)
wallet,
market,
market_instruction_builder,
desired_orders_chain,
order_reconciler,
args.redeem_threshold,
)
oracle_provider: mango.OracleProvider = mango.create_oracle_provider(context, args.oracle_provider)
oracle_market: mango.LoadedMarket = market if args.oracle_market is None else mango.load_market_by_symbol(
context, args.oracle_market)
oracle_provider: mango.OracleProvider = mango.create_oracle_provider(
context, args.oracle_provider
)
oracle_market: mango.LoadedMarket = (
market
if args.oracle_market is None
else mango.load_market_by_symbol(context, args.oracle_market)
)
oracle = oracle_provider.oracle_for_market(context, oracle_market)
if oracle is None:
raise Exception(f"Could not find oracle for market {oracle_market.symbol} from provider {args.oracle_provider}.")
raise Exception(
f"Could not find oracle for market {oracle_market.symbol} from provider {args.oracle_provider}."
)
model_state_builder: mango.marketmaking.ModelStateBuilder = mango.marketmaking.model_state_builder_factory(
args.update_mode, context, disposer, manager, health_check, wallet, group, account, market, oracle)
model_state_builder: mango.marketmaking.ModelStateBuilder = (
mango.marketmaking.model_state_builder_factory(
args.update_mode,
context,
disposer,
manager,
health_check,
wallet,
group,
account,
market,
oracle,
)
)
health_check.add("marketmaker_pulse", market_maker.pulse_complete)
logging.info(f"Current assets in account {account.address} (owner: {account.owner}):")
mango.InstrumentValue.report([asset for asset in account.net_values if asset is not None], logging.info)
mango.InstrumentValue.report(
[asset for asset in account.net_values if asset is not None], logging.info
)
manager.open()
@ -199,29 +335,50 @@ def hedging_pulse_action(_: int) -> None:
hedging_pulse_interval: float = args.hedging_pulse_interval or args.pulse_interval
separate_hedge_pulse = False
if isinstance(hedger, mango.hedging.NullHedger):
logging.info(f"Using a pulse action with an interval of {args.pulse_interval} seconds.")
logging.info(
f"Using a pulse action with an interval of {args.pulse_interval} seconds."
)
pulse_action = marketmaking_pulse_action
elif hedging_pulse_interval == args.pulse_interval:
logging.info(f"Using a combined pulse action with an interval of {args.pulse_interval} seconds.")
logging.info(
f"Using a combined pulse action with an interval of {args.pulse_interval} seconds."
)
pulse_action = combined_pulse_action
else:
logging.info(
f"Using separate pulse actions with a marketmaking interval of {args.pulse_interval} seconds and a hedging interval of {hedging_pulse_interval} seconds.")
f"Using separate pulse actions with a marketmaking interval of {args.pulse_interval} seconds and a hedging interval of {hedging_pulse_interval} seconds."
)
pulse_action = marketmaking_pulse_action
hedging_pulse_disposable = rx.interval(hedging_pulse_interval).pipe(
hedging_pulse_disposable = (
rx.interval(hedging_pulse_interval)
.pipe(
rx.operators.observe_on(context.create_thread_pool_scheduler()),
rx.operators.start_with(-1),
rx.operators.catch(mango.observable_pipeline_error_reporter),
rx.operators.retry(),
)
.subscribe(
mango.create_backpressure_skipping_observer(
on_next=hedging_pulse_action, on_error=mango.log_subscription_error
)
)
)
disposer.add_disposable(hedging_pulse_disposable)
marketmaking_pulse_disposable = (
rx.interval(args.pulse_interval)
.pipe(
rx.operators.observe_on(context.create_thread_pool_scheduler()),
rx.operators.start_with(-1),
rx.operators.catch(mango.observable_pipeline_error_reporter),
rx.operators.retry()
).subscribe(mango.create_backpressure_skipping_observer(on_next=hedging_pulse_action, on_error=mango.log_subscription_error))
disposer.add_disposable(hedging_pulse_disposable)
marketmaking_pulse_disposable = rx.interval(args.pulse_interval).pipe(
rx.operators.observe_on(context.create_thread_pool_scheduler()),
rx.operators.start_with(-1),
rx.operators.catch(mango.observable_pipeline_error_reporter),
rx.operators.retry()
).subscribe(mango.create_backpressure_skipping_observer(on_next=pulse_action, on_error=mango.log_subscription_error))
rx.operators.retry(),
)
.subscribe(
mango.create_backpressure_skipping_observer(
on_next=pulse_action, on_error=mango.log_subscription_error
)
)
)
disposer.add_disposable(marketmaking_pulse_disposable)
# Wait - don't exit. Exiting will be handled by signals/interrupts.

View File

@ -11,17 +11,23 @@ from spl.token.client import Token as SolanaSPLToken
from spl.token.constants import TOKEN_PROGRAM_ID
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), '..')))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="mint SPL tokens to your wallet")
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--symbol", type=str, required=True, help="token symbol to mint (e.g. USDC)")
parser.add_argument("--quantity", type=Decimal, required=True, help="quantity token to deposit")
parser.add_argument("--address", type=PublicKey,
help="Destination address for the minted token - can be either the actual token address or the address of the owner of the token address")
parser.add_argument(
"--symbol", type=str, required=True, help="token symbol to mint (e.g. USDC)"
)
parser.add_argument(
"--quantity", type=Decimal, required=True, help="quantity token to deposit"
)
parser.add_argument(
"--address",
type=PublicKey,
help="Destination address for the minted token - can be either the actual token address or the address of the owner of the token address",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
@ -32,21 +38,28 @@ if instrument is None:
raise Exception(f"Could not find instrument with symbol '{args.symbol}'.")
token: mango.Token = mango.Token.ensure(instrument)
spl_token = SolanaSPLToken(context.client.compatible_client, token.mint, TOKEN_PROGRAM_ID, wallet.keypair)
spl_token = SolanaSPLToken(
context.client.compatible_client, token.mint, TOKEN_PROGRAM_ID, wallet.keypair
)
# Is the address an actual token account? Or is it the SOL address of the owner?
account_info: typing.Optional[mango.AccountInfo] = mango.AccountInfo.load(context, args.address)
account_info: typing.Optional[mango.AccountInfo] = mango.AccountInfo.load(
context, args.address
)
if account_info is None:
raise Exception(f"Could not find account at address {args.address}.")
if account_info.owner == mango.SYSTEM_PROGRAM_ADDRESS:
# This is a root wallet account - get the associated token account
destination: PublicKey = mango.TokenAccount.find_or_create_token_address_to_use(
context, wallet, args.address, token)
context, wallet, args.address, token
)
quantity = token.shift_to_native(args.quantity)
mango.output(f"Minting {args.quantity} {args.symbol} to {destination}")
response = spl_token.mint_to(destination, wallet.address, int(quantity), multi_signers=[wallet.keypair])
response = spl_token.mint_to(
destination, wallet.address, int(quantity), multi_signers=[wallet.keypair]
)
mango.output(response["result"])

View File

@ -8,19 +8,29 @@ import sys
from decimal import Decimal
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(
description="Sends a notification if an account's SOL balance is below the '--minimum-sol-balance' parameter threshold.")
description="Sends a notification if an account's SOL balance is below the '--minimum-sol-balance' parameter threshold."
)
mango.ContextBuilder.add_command_line_parameters(parser)
parser.add_argument("--address", type=PublicKey, required=True,
help="address of the account")
parser.add_argument("--minimum-sol-balance", type=Decimal, default=Decimal("0.1"),
help="the minimum SOL balance required for the alert. A SOL balance less than this value will trigger a nifitication.")
parser.add_argument("--notify", type=mango.parse_notification_target, action="append", default=[],
help="The notification target for low balance events")
parser.add_argument(
"--address", type=PublicKey, required=True, help="address of the account"
)
parser.add_argument(
"--minimum-sol-balance",
type=Decimal,
default=Decimal("0.1"),
help="the minimum SOL balance required for the alert. A SOL balance less than this value will trigger a nifitication.",
)
parser.add_argument(
"--notify",
type=mango.parse_notification_target,
action="append",
default=[],
help="The notification target for low balance events",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
@ -31,6 +41,6 @@ if account_info is None:
else:
if account_info.sols < args.minimum_sol_balance:
notify: mango.NotificationTarget = mango.CompoundNotificationTarget(args.notify)
report = f"Account \"{args.name} [{args.address}]\" on {context.client.cluster_name} has only {account_info.sols} SOL, which is below the minimum required balance of {args.minimum_sol_balance} SOL."
report = f'Account "{args.name} [{args.address}]" on {context.client.cluster_name} has only {account_info.sols} SOL, which is below the minimum required balance of {args.minimum_sol_balance} SOL.'
notify.send(report)
mango.output(f"Notification sent: {report}")

View File

@ -8,35 +8,64 @@ import sys
from decimal import Decimal
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Shows all orders on a market.")
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--market", type=str, required=True, help="market symbol to buy (e.g. ETH/USDC)")
parser.add_argument("--quantity", type=Decimal, required=True, help="quantity to BUY or SELL")
parser.add_argument("--price", type=Decimal, required=True, help="price to BUY or SELL at")
parser.add_argument("--side", type=mango.Side, required=True, choices=list(mango.Side), help="side: BUY or SELL")
parser.add_argument("--order-type", type=mango.OrderType, required=True,
choices=list(mango.OrderType), help="Order type: LIMIT, IOC or POST_ONLY")
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(
"--market", type=str, required=True, help="market symbol to buy (e.g. ETH/USDC)"
)
parser.add_argument(
"--quantity", type=Decimal, required=True, help="quantity to BUY or SELL"
)
parser.add_argument(
"--price", type=Decimal, required=True, help="price to BUY or SELL at"
)
parser.add_argument(
"--side",
type=mango.Side,
required=True,
choices=list(mango.Side),
help="side: BUY or SELL",
)
parser.add_argument(
"--order-type",
type=mango.OrderType,
required=True,
choices=list(mango.OrderType),
help="Order type: LIMIT, IOC or POST_ONLY",
)
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",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
group = mango.Group.load(context, context.group_address)
account = mango.Account.load_for_owner_by_address(context, wallet.address, group, args.account_address)
account = mango.Account.load_for_owner_by_address(
context, wallet.address, group, args.account_address
)
market = context.market_lookup.find_by_symbol(args.market)
if market is None:
raise Exception(f"Could not find market {args.market}")
market_operations = mango.create_market_operations(context, wallet, account, market, args.dry_run)
order: mango.Order = mango.Order.from_basic_info(args.side, args.price, args.quantity, args.order_type)
market_operations = mango.create_market_operations(
context, wallet, account, market, args.dry_run
)
order: mango.Order = mango.Order.from_basic_info(
args.side, args.price, args.quantity, args.order_type
)
placed = market_operations.place_order(order)
mango.output(placed)

View File

@ -9,35 +9,49 @@ import typing
from decimal import Decimal
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), '..')))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
def report_accrued(basket_token: mango.AccountSlot) -> None:
symbol: str = basket_token.base_instrument.symbol
if basket_token.perp_account is None:
accrued: mango.InstrumentValue = mango.InstrumentValue(basket_token.base_instrument, Decimal(0))
accrued: mango.InstrumentValue = mango.InstrumentValue(
basket_token.base_instrument, Decimal(0)
)
else:
accrued = basket_token.perp_account.mngo_accrued
mango.output(f"Accrued in perp market [{symbol:>5}]: {accrued}")
def load_perp_market(context: mango.Context, group: mango.Group, slot: mango.GroupSlot) -> typing.Optional[mango.PerpMarket]:
def load_perp_market(
context: mango.Context, group: mango.Group, slot: mango.GroupSlot
) -> typing.Optional[mango.PerpMarket]:
if slot.perp_market is None:
return None
perp_market_details = mango.PerpMarketDetails.load(context, slot.perp_market.address, group)
perp_market = mango.PerpMarket(context.mango_program_address, slot.perp_market.address,
slot.base_instrument,
mango.Token.ensure(slot.quote_token_bank.token),
perp_market_details)
perp_market_details = mango.PerpMarketDetails.load(
context, slot.perp_market.address, group
)
perp_market = mango.PerpMarket(
context.mango_program_address,
slot.perp_market.address,
slot.base_instrument,
mango.Token.ensure(slot.quote_token_bank.token),
perp_market_details,
)
return perp_market
def find_basket_token_in_account(account: mango.Account, instrument: mango.Instrument) -> typing.Optional[mango.AccountSlot]:
basket_tokens = [in_basket for in_basket in account.slots if in_basket.base_instrument == instrument]
def find_basket_token_in_account(
account: mango.Account, instrument: mango.Instrument
) -> typing.Optional[mango.AccountSlot]:
basket_tokens = [
in_basket
for in_basket in account.slots
if in_basket.base_instrument == instrument
]
if len(basket_tokens) == 0:
return None
@ -45,40 +59,70 @@ def find_basket_token_in_account(account: mango.Account, instrument: mango.Instr
return basket_tokens[0]
def build_redeem_instruction_for_account(context: mango.Context, wallet: mango.Wallet, group: mango.Group,
mngo: mango.TokenBank, account: mango.Account,
perp_market: mango.PerpMarket,
basket_token: typing.Optional[mango.AccountSlot]) -> mango.CombinableInstructions:
if (basket_token is None) or (basket_token.perp_account is None) or basket_token.perp_account.mngo_accrued.value == 0:
def build_redeem_instruction_for_account(
context: mango.Context,
wallet: mango.Wallet,
group: mango.Group,
mngo: mango.TokenBank,
account: mango.Account,
perp_market: mango.PerpMarket,
basket_token: typing.Optional[mango.AccountSlot],
) -> mango.CombinableInstructions:
if (
(basket_token is None)
or (basket_token.perp_account is None)
or basket_token.perp_account.mngo_accrued.value == 0
):
return mango.CombinableInstructions.empty()
report_accrued(basket_token)
redeem = mango.build_redeem_accrued_mango_instructions(context, wallet, perp_market, group, account, mngo)
redeem = mango.build_redeem_accrued_mango_instructions(
context, wallet, perp_market, group, account, mngo
)
return redeem
parser = argparse.ArgumentParser(description="redeems accrued MNGO from a Mango account")
parser = argparse.ArgumentParser(
description="redeems accrued MNGO from 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 with accrued MNGO (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("--wait", action="store_true", default=False,
help="wait until the transaction is confirmed")
parser.add_argument(
"--market", type=str, help="perp market symbol with accrued MNGO (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(
"--wait",
action="store_true",
default=False,
help="wait until the transaction is 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")
raise Exception(
"Must specify either an individual market (using --market) or use --all for all markets"
)
context = mango.ContextBuilder.from_command_line_parameters(args)
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
group = mango.Group.load(context, context.group_address)
mngo = group.liquidity_incentive_token_bank
account = mango.Account.load_for_owner_by_address(context, wallet.address, group, args.account_address)
account = mango.Account.load_for_owner_by_address(
context, wallet.address, group, args.account_address
)
signers: mango.CombinableInstructions = mango.CombinableInstructions.from_wallet(wallet)
all_instructions: mango.CombinableInstructions = signers
@ -89,7 +133,8 @@ if args.all:
if perp_market is not None:
basket_token = find_basket_token_in_account(account, slot.base_instrument)
all_instructions += build_redeem_instruction_for_account(
context, wallet, group, mngo, account, perp_market, basket_token)
context, wallet, group, mngo, account, perp_market, basket_token
)
else:
market = context.market_lookup.find_by_symbol(args.market)
if market is None:
@ -101,18 +146,23 @@ else:
perp_market = loaded_market
basket_token = find_basket_token_in_account(account, perp_market.base)
all_instructions += build_redeem_instruction_for_account(context,
wallet, group, mngo, account, perp_market, basket_token)
all_instructions += build_redeem_instruction_for_account(
context, wallet, group, mngo, account, perp_market, basket_token
)
transaction_ids = all_instructions.execute(context)
mango.output("Transaction IDs:", transaction_ids)
if args.wait:
context.client.wait_for_confirmation(transaction_ids)
reloaded_account = mango.Account.load_for_owner_by_address(context, wallet.address, group, args.account_address)
reloaded_account = mango.Account.load_for_owner_by_address(
context, wallet.address, group, args.account_address
)
if args.all:
for slot in group.slots:
basket_token = find_basket_token_in_account(reloaded_account, slot.base_instrument)
basket_token = find_basket_token_in_account(
reloaded_account, slot.base_instrument
)
if basket_token is not None:
report_accrued(basket_token)
elif perp_market is not None:

View File

@ -7,29 +7,44 @@ import sys
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Register a referrer ID for a Mango Account.")
parser = argparse.ArgumentParser(
description="Register a referrer ID for a Mango Account."
)
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--account-address", type=PublicKey,
help="address of the specific account to use, if more than one available")
parser.add_argument("--id", type=str, required=True,
help="referrer ID to register - must be no longer than 32 characters")
parser.add_argument(
"--account-address",
type=PublicKey,
help="address of the specific account to use, if more than one available",
)
parser.add_argument(
"--id",
type=str,
required=True,
help="referrer ID to register - must be no longer than 32 characters",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
group = mango.Group.load(context, context.group_address)
account = mango.Account.load_for_owner_by_address(context, wallet.address, group, args.account_address)
account = mango.Account.load_for_owner_by_address(
context, wallet.address, group, args.account_address
)
all_instructions: mango.CombinableInstructions = mango.CombinableInstructions.from_signers([wallet.keypair])
all_instructions: mango.CombinableInstructions = (
mango.CombinableInstructions.from_signers([wallet.keypair])
)
referrer_record_address: PublicKey = group.derive_referrer_record_address(context, args.id)
referrer_record_address: PublicKey = group.derive_referrer_record_address(
context, args.id
)
set_delegate_instructions = mango.build_register_referrer_id_instructions(
context, wallet, group, account, referrer_record_address, args.id)
context, wallet, group, account, referrer_record_address, args.id
)
all_instructions += set_delegate_instructions
transaction_ids = all_instructions.execute(context)

View File

@ -14,40 +14,79 @@ import typing
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
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="Run the Transaction Scout to display information about a specific transaction.")
description="Run the Transaction Scout to display information about a specific transaction."
)
mango.ContextBuilder.add_command_line_parameters(parser)
parser.add_argument("--since-state-filename", type=str, default="report.state",
help="The name of the state file containing the signature of the last transaction looked up")
parser.add_argument("--instruction-type", type=lambda ins: mango.InstructionType[ins], required=True,
choices=list(mango.InstructionType),
help="The signature of the transaction to look up")
parser.add_argument("--sender", type=PublicKey,
help="Only transactions sent by this PublicKey will be returned")
parser.add_argument("--notify-transactions", type=mango.parse_notification_target, action="append", default=[],
help="The notification target for transaction information")
parser.add_argument("--notify-successful-transactions", type=mango.parse_notification_target,
action="append", default=[], help="The notification target for successful transactions")
parser.add_argument("--notify-failed-transactions", type=mango.parse_notification_target,
action="append", default=[], help="The notification target for failed transactions")
parser.add_argument("--notify-errors", type=mango.parse_notification_target, action="append", default=[],
help="The notification target for errors")
parser.add_argument("--summarise", action="store_true", default=False,
help="create a short summary rather than the full TransactionScout details")
parser.add_argument(
"--since-state-filename",
type=str,
default="report.state",
help="The name of the state file containing the signature of the last transaction looked up",
)
parser.add_argument(
"--instruction-type",
type=lambda ins: mango.InstructionType[ins],
required=True,
choices=list(mango.InstructionType),
help="The signature of the transaction to look up",
)
parser.add_argument(
"--sender",
type=PublicKey,
help="Only transactions sent by this PublicKey will be returned",
)
parser.add_argument(
"--notify-transactions",
type=mango.parse_notification_target,
action="append",
default=[],
help="The notification target for transaction information",
)
parser.add_argument(
"--notify-successful-transactions",
type=mango.parse_notification_target,
action="append",
default=[],
help="The notification target for successful transactions",
)
parser.add_argument(
"--notify-failed-transactions",
type=mango.parse_notification_target,
action="append",
default=[],
help="The notification target for failed transactions",
)
parser.add_argument(
"--notify-errors",
type=mango.parse_notification_target,
action="append",
default=[],
help="The notification target for errors",
)
parser.add_argument(
"--summarise",
action="store_true",
default=False,
help="create a short summary rather than the full TransactionScout details",
)
args: argparse.Namespace = mango.parse_args(parser)
handler = mango.NotificationHandler(mango.CompoundNotificationTarget(args.notify_errors))
handler = mango.NotificationHandler(
mango.CompoundNotificationTarget(args.notify_errors)
)
handler.setLevel(logging.ERROR)
logging.getLogger().addHandler(handler)
def summariser(context: mango.Context) -> typing.Callable[[mango.TransactionScout], str]:
def summariser(
context: mango.Context,
) -> typing.Callable[[mango.TransactionScout], str]:
def summarise(transaction_scout: mango.TransactionScout) -> str:
instruction_details: typing.List[str] = []
instruction_targets: typing.List[str] = []
@ -64,24 +103,41 @@ def summariser(context: mango.Context) -> typing.Callable[[mango.TransactionScou
instructions = ", ".join(instruction_details)
targets = ", ".join(instruction_targets) or "None"
changes = mango.OwnedInstrumentValue.changes(
transaction_scout.pre_token_balances, transaction_scout.post_token_balances)
transaction_scout.pre_token_balances, transaction_scout.post_token_balances
)
in_tokens = []
for ins in transaction_scout.instructions:
if ins.token_in_account is not None:
in_tokens += [mango.OwnedInstrumentValue.find_by_owner(changes, ins.token_in_account)]
in_tokens += [
mango.OwnedInstrumentValue.find_by_owner(
changes, ins.token_in_account
)
]
out_tokens = []
for ins in transaction_scout.instructions:
if ins.token_out_account is not None:
out_tokens += [mango.OwnedInstrumentValue.find_by_owner(changes, ins.token_out_account)]
out_tokens += [
mango.OwnedInstrumentValue.find_by_owner(
changes, ins.token_out_account
)
]
changed_tokens = in_tokens + out_tokens
changed_tokens_text = ", ".join(
[f"{tok.token_value.value:,.8f} {tok.token_value.token.name}" for tok in changed_tokens]) or "None"
changed_tokens_text = (
", ".join(
[
f"{tok.token_value.value:,.8f} {tok.token_value.token.name}"
for tok in changed_tokens
]
)
or "None"
)
success_marker = "✅" if transaction_scout.succeeded else "❌"
return f"« 🥭 {transaction_scout.timestamp} {success_marker} {transaction_scout.group_name} {instructions}\n From: {transaction_scout.sender}\n Target(s): {targets}\n Token Changes: {changed_tokens_text}\n {transaction_scout.signatures} »"
return summarise
@ -101,8 +157,12 @@ try:
first_item_capturer = mango.CaptureFirstItem()
signatures = mango.fetch_all_recent_transaction_signatures(context)
oldest_first = reversed(list(itertools.takewhile(lambda sig: sig != since_signature, signatures)))
pipeline: rx.core.typing.Observable[mango.TransactionScout] = rx.from_(oldest_first).pipe(
oldest_first = reversed(
list(itertools.takewhile(lambda sig: sig != since_signature, signatures))
)
pipeline: rx.core.typing.Observable[mango.TransactionScout] = rx.from_(
oldest_first
).pipe(
ops.map(first_item_capturer.capture_if_first),
# ops.map(debug_print_item("Signature:")),
ops.map(lambda sig: mango.TransactionScout.load_if_available(context, sig)),
@ -110,19 +170,17 @@ try:
)
if sender is not None:
pipeline = pipeline.pipe(
ops.filter(lambda item: bool(item.sender == sender))
)
pipeline = pipeline.pipe(ops.filter(lambda item: bool(item.sender == sender)))
if instruction_type is not None:
pipeline = pipeline.pipe(
ops.filter(lambda item: bool(item.has_any_instruction_of_type(instruction_type)))
ops.filter(
lambda item: bool(item.has_any_instruction_of_type(instruction_type))
)
)
if args.summarise:
pipeline = pipeline.pipe(
ops.map(summariser(context))
)
pipeline = pipeline.pipe(ops.map(summariser(context)))
fan_out: rx.subject.subject.Subject = rx.subject.subject.Subject()
fan_out.subscribe(mango.PrintingObserverSubscriber(False))
@ -130,12 +188,14 @@ try:
on_success = mango.FilteringNotificationTarget(
mango.CompoundNotificationTarget(args.notify_successful_transactions),
lambda item: isinstance(item, mango.TransactionScout) and item.succeeded)
lambda item: isinstance(item, mango.TransactionScout) and item.succeeded,
)
fan_out.subscribe(on_next=on_success.send) # type: ignore[call-arg]
on_failed = mango.FilteringNotificationTarget(
mango.CompoundNotificationTarget(args.notify_failed_transactions),
lambda item: isinstance(item, mango.TransactionScout) and not item.succeeded)
lambda item: isinstance(item, mango.TransactionScout) and not item.succeeded,
)
fan_out.subscribe(on_next=on_failed.send) # type: ignore[call-arg]
pipeline.subscribe(fan_out)
@ -145,7 +205,9 @@ try:
state_file.write(signatures[0])
except Exception as exception:
logging.critical(
f"report-transactions stopped because of exception: {exception} - {traceback.format_exc()}")
f"report-transactions stopped because of exception: {exception} - {traceback.format_exc()}"
)
except:
logging.critical(
f"report-transactions stopped because of uncatchable error: {traceback.format_exc()}")
f"report-transactions stopped because of uncatchable error: {traceback.format_exc()}"
)

View File

@ -7,25 +7,35 @@ import os.path
import sys
import traceback
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
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="Sends SOL to a different address.")
parser.add_argument("--notification-target", type=mango.parse_notification_target, required=True, action="append",
help="The notification target - a compound string that varies depending on the target")
parser.add_argument(
"--notification-target",
type=mango.parse_notification_target,
required=True,
action="append",
help="The notification target - a compound string that varies depending on the target",
)
parser.add_argument("--message", type=str, help="Message to send")
args: argparse.Namespace = mango.parse_args(parser)
try:
notify: mango.NotificationTarget = mango.CompoundNotificationTarget(args.notification_target)
notify: mango.NotificationTarget = mango.CompoundNotificationTarget(
args.notification_target
)
mango.output("Sending to:", notify)
notify.send(args.message)
mango.output("Notifications sent")
except Exception as exception:
logging.critical(f"send-notification stopped because of exception: {exception} - {traceback.format_exc()}")
logging.critical(
f"send-notification stopped because of exception: {exception} - {traceback.format_exc()}"
)
except:
logging.critical(f"send-notification stopped because of uncatchable error: {traceback.format_exc()}")
logging.critical(
f"send-notification stopped because of uncatchable error: {traceback.format_exc()}"
)

View File

@ -12,8 +12,7 @@ from solana.publickey import PublicKey
from solana.system_program import TransferParams, transfer
from solana.transaction import Transaction
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
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
@ -21,11 +20,20 @@ import mango # nopep8
parser = argparse.ArgumentParser(description="Sends SOL to a different address.")
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
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")
parser.add_argument("--quantity", type=Decimal, required=True, help="quantity of token to send")
parser.add_argument("--dry-run", action="store_true", default=False,
help="runs as read-only and does not perform any transactions")
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",
)
parser.add_argument(
"--quantity", type=Decimal, required=True, help="quantity of token to send"
)
parser.add_argument(
"--dry-run",
action="store_true",
default=False,
help="runs as read-only and does not perform any transactions",
)
args: argparse.Namespace = mango.parse_args(parser)
try:
@ -51,7 +59,9 @@ try:
mango.output("Skipping actual transfer - dry run.")
else:
transaction = Transaction()
params = TransferParams(from_pubkey=source, to_pubkey=destination, lamports=lamports)
params = TransferParams(
from_pubkey=source, to_pubkey=destination, lamports=lamports
)
transaction.add(transfer(params))
transaction_id = context.client.send_transaction(transaction, wallet.keypair)
@ -61,6 +71,10 @@ try:
updated_balance = context.client.get_balance(wallet.address)
mango.output(f"{text_amount} sent. Balance now: {updated_balance} SOL")
except Exception as exception:
logging.critical(f"send-sols stopped because of exception: {exception} - {traceback.format_exc()}")
logging.critical(
f"send-sols stopped because of exception: {exception} - {traceback.format_exc()}"
)
except:
logging.critical(f"send-sols stopped because of uncatchable error: {traceback.format_exc()}")
logging.critical(
f"send-sols stopped because of uncatchable error: {traceback.format_exc()}"
)

View File

@ -13,8 +13,7 @@ from solana.rpc.types import TxOpts
from spl.token.client import Token as SolanaSPLToken
from spl.token.constants import ACCOUNT_LEN, TOKEN_PROGRAM_ID
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
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
@ -22,14 +21,29 @@ import mango # nopep8
parser = argparse.ArgumentParser(description="Sends SPL tokens to a different address.")
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--symbol", type=str, required=True, help="token symbol to send (e.g. ETH)")
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")
parser.add_argument("--quantity", type=Decimal, required=True, help="quantity of token to send")
parser.add_argument("--wait", action="store_true", default=False,
help="wait until the transaction is confirmed")
parser.add_argument("--dry-run", action="store_true", default=False,
help="runs as read-only and does not perform any transactions")
parser.add_argument(
"--symbol", type=str, required=True, help="token symbol to send (e.g. ETH)"
)
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",
)
parser.add_argument(
"--quantity", type=Decimal, required=True, help="quantity of token to send"
)
parser.add_argument(
"--wait",
action="store_true",
default=False,
help="wait until the transaction is confirmed",
)
parser.add_argument(
"--dry-run",
action="store_true",
default=False,
help="runs as read-only and does not perform any transactions",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
@ -42,31 +56,44 @@ if instrument is None:
raise Exception(f"Could not find details of token with symbol {args.symbol}.")
token: mango.Token = mango.Token.ensure(instrument)
spl_token = SolanaSPLToken(context.client.compatible_client, token.mint, TOKEN_PROGRAM_ID, wallet.keypair)
spl_token = SolanaSPLToken(
context.client.compatible_client, token.mint, TOKEN_PROGRAM_ID, wallet.keypair
)
source_accounts = spl_token.get_accounts(wallet.address)
source_account = source_accounts["result"]["value"][0]
source = PublicKey(source_account["pubkey"])
# Is the address an actual token account? Or is it the SOL address of the owner?
account_info: typing.Optional[mango.AccountInfo] = mango.AccountInfo.load(context, args.address)
account_info: typing.Optional[mango.AccountInfo] = mango.AccountInfo.load(
context, args.address
)
if account_info is None:
raise Exception(f"Could not find account at address {args.address}.")
destination: PublicKey
if account_info.owner == mango.SYSTEM_PROGRAM_ADDRESS:
# This is a root wallet account - get the token account to use.
destination = mango.TokenAccount.find_or_create_token_address_to_use(context, wallet, args.address, token)
destination = mango.TokenAccount.find_or_create_token_address_to_use(
context, wallet, args.address, token
)
elif account_info.owner == TOKEN_PROGRAM_ID and len(account_info.data) == ACCOUNT_LEN:
# This is not a root wallet account, this is an SPL token account.
destination = args.address
else:
raise Exception(f"Account {args.address} is neither a root wallet account nor an SPL token account.")
raise Exception(
f"Account {args.address} is neither a root wallet account nor an SPL token account."
)
owner = wallet.keypair
amount = int(args.quantity * Decimal(10 ** token.decimals))
amount = int(args.quantity * Decimal(10**token.decimals))
mango.output("Balance:", source_account["account"]["data"]["parsed"]
["info"]["tokenAmount"]["uiAmountString"], token.name)
mango.output(
"Balance:",
source_account["account"]["data"]["parsed"]["info"]["tokenAmount"][
"uiAmountString"
],
token.name,
)
text_amount = f"{amount} {token.name} (@ {token.decimals} decimal places)"
mango.output(f"Sending {text_amount}")
mango.output(f" From: {source}")
@ -75,8 +102,13 @@ mango.output(f" To: {destination}")
if args.dry_run:
mango.output("Skipping actual transfer - dry run.")
else:
transfer_response = spl_token.transfer(source, destination, owner, amount,
opts=TxOpts(preflight_commitment=context.client.commitment))
transfer_response = spl_token.transfer(
source,
destination,
owner,
amount,
opts=TxOpts(preflight_commitment=context.client.commitment),
)
transaction_ids = [transfer_response["result"]]
mango.output(f"Transaction IDs: {transaction_ids}")
if args.wait:
@ -84,6 +116,8 @@ else:
updated_balance = spl_token.get_balance(source)
updated_balance_text = updated_balance["result"]["value"]["uiAmountString"]
mango.output(f"{text_amount} sent. Balance now: {updated_balance_text} {token.name}")
mango.output(
f"{text_amount} sent. Balance now: {updated_balance_text} {token.name}"
)
else:
mango.output(f"{text_amount} sent.")

View File

@ -9,8 +9,7 @@ import traceback
from decimal import Decimal
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
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
@ -18,14 +17,30 @@ import mango # nopep8
parser = argparse.ArgumentParser(description="Buys an SPL token in a Serum market.")
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--symbol", type=str, required=True, help="market symbol to buy (e.g. ETH/USDC)")
parser.add_argument("--quantity", type=Decimal, required=True, help="quantity of token to buy")
parser.add_argument("--adjustment-factor", type=Decimal, default=Decimal("0.05"),
help="factor by which to adjust the BUY price (akin to maximum slippage)")
parser.add_argument("--wait", action="store_true", default=False,
help="wait until the transaction is confirmed")
parser.add_argument("--dry-run", action="store_true", default=False,
help="runs as read-only and does not perform any transactions")
parser.add_argument(
"--symbol", type=str, required=True, help="market symbol to buy (e.g. ETH/USDC)"
)
parser.add_argument(
"--quantity", type=Decimal, required=True, help="quantity of token to buy"
)
parser.add_argument(
"--adjustment-factor",
type=Decimal,
default=Decimal("0.05"),
help="factor by which to adjust the BUY price (akin to maximum slippage)",
)
parser.add_argument(
"--wait",
action="store_true",
default=False,
help="wait until the transaction is confirmed",
)
parser.add_argument(
"--dry-run",
action="store_true",
default=False,
help="runs as read-only and does not perform any transactions",
)
args: argparse.Namespace = mango.parse_args(parser)
try:
@ -39,11 +54,17 @@ try:
if args.dry_run:
trade_executor: mango.TradeExecutor = mango.NullTradeExecutor()
else:
trade_executor = mango.ImmediateTradeExecutor(context, wallet, None, adjustment_factor)
trade_executor = mango.ImmediateTradeExecutor(
context, wallet, None, adjustment_factor
)
order = trade_executor.buy(args.symbol, args.quantity)
logging.info(f"Buy completed for {order}")
except Exception as exception:
logging.critical(f"Buy stopped because of exception: {exception} - {traceback.format_exc()}")
logging.critical(
f"Buy stopped because of exception: {exception} - {traceback.format_exc()}"
)
except:
logging.critical(f"Buy stopped because of uncatchable error: {traceback.format_exc()}")
logging.critical(
f"Buy stopped because of uncatchable error: {traceback.format_exc()}"
)

View File

@ -9,8 +9,7 @@ import traceback
from decimal import Decimal
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
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
@ -18,14 +17,30 @@ import mango # nopep8
parser = argparse.ArgumentParser(description="Sells an SPL token in a Serum market.")
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--symbol", type=str, required=True, help="market symbol to buy (e.g. ETH/USDC)")
parser.add_argument("--quantity", type=Decimal, required=True, help="quantity of token to buy")
parser.add_argument("--adjustment-factor", type=Decimal, default=Decimal("0.05"),
help="factor by which to adjust the SELL price (akin to maximum slippage)")
parser.add_argument("--wait", action="store_true", default=False,
help="wait until the transaction is confirmed")
parser.add_argument("--dry-run", action="store_true", default=False,
help="runs as read-only and does not perform any transactions")
parser.add_argument(
"--symbol", type=str, required=True, help="market symbol to buy (e.g. ETH/USDC)"
)
parser.add_argument(
"--quantity", type=Decimal, required=True, help="quantity of token to buy"
)
parser.add_argument(
"--adjustment-factor",
type=Decimal,
default=Decimal("0.05"),
help="factor by which to adjust the SELL price (akin to maximum slippage)",
)
parser.add_argument(
"--wait",
action="store_true",
default=False,
help="wait until the transaction is confirmed",
)
parser.add_argument(
"--dry-run",
action="store_true",
default=False,
help="runs as read-only and does not perform any transactions",
)
args: argparse.Namespace = mango.parse_args(parser)
try:
@ -39,11 +54,17 @@ try:
if args.dry_run:
trade_executor: mango.TradeExecutor = mango.NullTradeExecutor()
else:
trade_executor = mango.ImmediateTradeExecutor(context, wallet, None, adjustment_factor)
trade_executor = mango.ImmediateTradeExecutor(
context, wallet, None, adjustment_factor
)
order = trade_executor.sell(args.symbol, args.quantity)
logging.info(f"Sell completed for {order}")
except Exception as exception:
logging.critical(f"Buy stopped because of exception: {exception} - {traceback.format_exc()}")
logging.critical(
f"Buy stopped because of exception: {exception} - {traceback.format_exc()}"
)
except:
logging.critical(f"Buy stopped because of uncatchable error: {traceback.format_exc()}")
logging.critical(
f"Buy stopped because of uncatchable error: {traceback.format_exc()}"
)

View File

@ -7,30 +7,41 @@ import sys
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Sets the referrer for a Mango Account.")
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--account-address", type=PublicKey,
help="address of the specific account to use, if more than one available")
parser.add_argument("--referrer-address", type=PublicKey, required=True,
help="address of the referrer's Mango Account")
parser.add_argument(
"--account-address",
type=PublicKey,
help="address of the specific account to use, if more than one available",
)
parser.add_argument(
"--referrer-address",
type=PublicKey,
required=True,
help="address of the referrer's Mango Account",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
group = mango.Group.load(context, context.group_address)
account = mango.Account.load_for_owner_by_address(context, wallet.address, group, args.account_address)
account = mango.Account.load_for_owner_by_address(
context, wallet.address, group, args.account_address
)
referrer_account = mango.Account.load(context, args.referrer_address, group)
all_instructions: mango.CombinableInstructions = mango.CombinableInstructions.from_signers([wallet.keypair])
all_instructions: mango.CombinableInstructions = (
mango.CombinableInstructions.from_signers([wallet.keypair])
)
referrer_memory_address: PublicKey = account.derive_referrer_memory_address(context)
set_delegate_instructions = mango.build_set_referrer_memory_instructions(
context, wallet, group, account, referrer_memory_address, referrer_account.address)
context, wallet, group, account, referrer_memory_address, referrer_account.address
)
all_instructions += set_delegate_instructions
transaction_ids = all_instructions.execute(context)

View File

@ -8,26 +8,41 @@ import sys
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), '..')))
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="Settles all openorders transactions in the Group.")
parser = argparse.ArgumentParser(
description="Settles all openorders transactions in the Group."
)
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--market", type=str, required=True, help="market symbol to make market upon (e.g. ETH/USDC)")
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(
"--market",
type=str,
required=True,
help="market symbol to make market upon (e.g. ETH/USDC)",
)
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",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
group = mango.Group.load(context, context.group_address)
account = mango.Account.load_for_owner_by_address(context, wallet.address, group, args.account_address)
account = mango.Account.load_for_owner_by_address(
context, wallet.address, group, args.account_address
)
logging.info(f"Wallet address: {wallet.address}")
@ -35,7 +50,9 @@ market = context.market_lookup.find_by_symbol(args.market)
if market is None:
raise Exception(f"Could not find market {args.market}")
market_operations = mango.create_market_operations(context, wallet, account, market, args.dry_run)
market_operations = mango.create_market_operations(
context, wallet, account, market, args.dry_run
)
settle = market_operations.settle()
mango.output(settle)

View File

@ -10,15 +10,19 @@ import typing
from decimal import Decimal
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Display the balances of all group tokens in the current wallet.")
parser = argparse.ArgumentParser(
description="Display the balances of all group tokens in the current wallet."
)
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--address", type=PublicKey,
help="Root address to check (if not provided, the wallet address is used)")
parser.add_argument(
"--address",
type=PublicKey,
help="Root address to check (if not provided, the wallet address is used)",
)
args: argparse.Namespace = mango.parse_args(parser)
address: typing.Optional[PublicKey] = args.address
@ -38,11 +42,15 @@ balances += [mango.InstrumentValue(mango.SolToken, sol_balance)]
for slot_token_bank in group.tokens:
if isinstance(slot_token_bank.token, mango.Token):
balance = mango.InstrumentValue.fetch_total_value(context, address, slot_token_bank.token)
balance = mango.InstrumentValue.fetch_total_value(
context, address, slot_token_bank.token
)
balances += [balance]
mango.output(f"\nToken Balances [{address}]:")
total_in_wallet: mango.InstrumentValue = mango.InstrumentValue(group.shared_quote_token, Decimal(0))
total_in_wallet: mango.InstrumentValue = mango.InstrumentValue(
group.shared_quote_token, Decimal(0)
)
for balance in balances:
if balance.value != 0:
balance_text: str = f"{balance} "
@ -51,57 +59,86 @@ for balance in balances:
total_in_wallet = total_in_wallet + balance
value_text = f" worth {balance}"
else:
slot: typing.Optional[mango.GroupSlot] = group.slot_by_instrument_or_none(balance.token)
slot: typing.Optional[mango.GroupSlot] = group.slot_by_instrument_or_none(
balance.token
)
if slot is not None:
cached_token_price: mango.InstrumentValue = group.token_price_from_cache(cache, slot.base_instrument)
cached_token_price: mango.InstrumentValue = (
group.token_price_from_cache(cache, slot.base_instrument)
)
balance_value: mango.InstrumentValue = balance * cached_token_price
total_in_wallet += balance_value
value_text = f" worth {balance_value}"
mango.output(f" {balance_text:<45}{value_text}")
mango.output(
f"Total Value: {total_in_wallet}")
mango.output(f"Total Value: {total_in_wallet}")
mango_accounts = mango.Account.load_all_for_owner(context, address, group)
account_value: mango.InstrumentValue = mango.InstrumentValue(group.shared_quote_token, Decimal(0))
quote_token_free_in_open_orders: mango.InstrumentValue = mango.InstrumentValue(group.shared_quote_token, Decimal(0))
quote_token_total_in_open_orders: mango.InstrumentValue = mango.InstrumentValue(group.shared_quote_token, Decimal(0))
account_value: mango.InstrumentValue = mango.InstrumentValue(
group.shared_quote_token, Decimal(0)
)
quote_token_free_in_open_orders: mango.InstrumentValue = mango.InstrumentValue(
group.shared_quote_token, Decimal(0)
)
quote_token_total_in_open_orders: mango.InstrumentValue = mango.InstrumentValue(
group.shared_quote_token, Decimal(0)
)
grand_total: mango.InstrumentValue = total_in_wallet
for account in mango_accounts:
mango.output("\n⚠ WARNING! ⚠ This is a work-in-progress and these figures may be wrong!\n")
mango.output(
"\n⚠ WARNING! ⚠ This is a work-in-progress and these figures may be wrong!\n"
)
mango.output(f"\nAccount Balances [{account.address}]:")
at_least_one_output: bool = False
open_orders: typing.Dict[str, mango.OpenOrders] = account.load_all_spot_open_orders(context)
open_orders: typing.Dict[str, mango.OpenOrders] = account.load_all_spot_open_orders(
context
)
for asset in account.base_slots:
if (asset.deposit.value != 0) or (asset.borrow.value != 0) or (asset.net_value.value != 0) or ((asset.perp_account is not None) and not asset.perp_account.empty):
if (
(asset.deposit.value != 0)
or (asset.borrow.value != 0)
or (asset.net_value.value != 0)
or ((asset.perp_account is not None) and not asset.perp_account.empty)
):
at_least_one_output = True
report: mango.AccountInstrumentValues = mango.AccountInstrumentValues.from_account_basket_base_token(
asset, open_orders, group)
report: mango.AccountInstrumentValues = (
mango.AccountInstrumentValues.from_account_basket_base_token(
asset, open_orders, group
)
)
# mango.output(report)
market_cache: mango.MarketCache = group.market_cache_from_cache(cache, report.base_token)
price_from_cache: mango.InstrumentValue = group.token_price_from_cache(cache, report.base_token)
market_cache: mango.MarketCache = group.market_cache_from_cache(
cache, report.base_token
)
price_from_cache: mango.InstrumentValue = group.token_price_from_cache(
cache, report.base_token
)
priced_report: mango.AccountInstrumentValues = report.priced(market_cache)
account_value += priced_report.net_value
quote_token_free_in_open_orders += priced_report.quote_token_free
quote_token_total_in_open_orders += priced_report.quote_token_total
mango.output(priced_report)
quote_report = mango.AccountInstrumentValues(account.shared_quote_token,
account.shared_quote_token,
account.shared_quote.raw_deposit,
account.shared_quote.deposit,
account.shared_quote.raw_borrow,
account.shared_quote.borrow,
mango.InstrumentValue(group.shared_quote_token, Decimal(0)),
mango.InstrumentValue(group.shared_quote_token, Decimal(0)),
quote_token_free_in_open_orders,
quote_token_total_in_open_orders,
mango.InstrumentValue(group.shared_quote_token, Decimal(0)),
Decimal(0), Decimal(0),
mango.InstrumentValue(group.shared_quote_token, Decimal(0)),
mango.InstrumentValue(group.shared_quote_token, Decimal(0)),
Decimal(0), Decimal(0),
mango.NullLotSizeConverter())
quote_report = mango.AccountInstrumentValues(
account.shared_quote_token,
account.shared_quote_token,
account.shared_quote.raw_deposit,
account.shared_quote.deposit,
account.shared_quote.raw_borrow,
account.shared_quote.borrow,
mango.InstrumentValue(group.shared_quote_token, Decimal(0)),
mango.InstrumentValue(group.shared_quote_token, Decimal(0)),
quote_token_free_in_open_orders,
quote_token_total_in_open_orders,
mango.InstrumentValue(group.shared_quote_token, Decimal(0)),
Decimal(0),
Decimal(0),
mango.InstrumentValue(group.shared_quote_token, Decimal(0)),
mango.InstrumentValue(group.shared_quote_token, Decimal(0)),
Decimal(0),
Decimal(0),
mango.NullLotSizeConverter(),
)
account_value += quote_report.net_value + quote_token_total_in_open_orders
mango.output(quote_report)

View File

@ -9,15 +9,19 @@ import typing
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Display the balances of all group tokens in the current wallet.")
parser = argparse.ArgumentParser(
description="Display the balances of all group tokens in the current wallet."
)
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--address", type=PublicKey,
help="Root address to check (if not provided, the wallet address is used)")
parser.add_argument(
"--address",
type=PublicKey,
help="Root address to check (if not provided, the wallet address is used)",
)
args: argparse.Namespace = mango.parse_args(parser)
address: typing.Optional[PublicKey] = args.address
@ -29,7 +33,9 @@ context: mango.Context = mango.ContextBuilder.from_command_line_parameters(args)
group: mango.Group = mango.Group.load(context)
cache: mango.Cache = mango.Cache.load(context, group.cache)
address_account_info: typing.Optional[mango.AccountInfo] = mango.AccountInfo.load(context, address)
address_account_info: typing.Optional[mango.AccountInfo] = mango.AccountInfo.load(
context, address
)
if address_account_info is None:
raise Exception(f"Could not load account data from address {address}")
@ -40,12 +46,16 @@ else:
mango_accounts = mango.Account.load_all_for_owner(context, address, group)
for account in mango_accounts:
mango.output("\n⚠ WARNING! ⚠ This is a work-in-progress and these figures may be wrong!\n")
pandas.set_option('display.max_columns', None)
pandas.set_option('display.width', None)
pandas.set_option('precision', 6)
mango.output(
"\n⚠ WARNING! ⚠ This is a work-in-progress and these figures may be wrong!\n"
)
pandas.set_option("display.max_columns", None)
pandas.set_option("display.width", None)
pandas.set_option("precision", 6)
open_orders: typing.Dict[str, mango.OpenOrders] = account.load_all_spot_open_orders(context)
open_orders: typing.Dict[str, mango.OpenOrders] = account.load_all_spot_open_orders(
context
)
frame: pandas.DataFrame = account.to_dataframe(group, open_orders, cache)
mango.output(frame)

View File

@ -7,15 +7,22 @@ import sys
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Shows the on-chain data of a particular account.")
parser = argparse.ArgumentParser(
description="Shows the on-chain data of a particular account."
)
mango.ContextBuilder.add_command_line_parameters(parser)
parser.add_argument("--address", type=PublicKey, required=True, help="address of the account")
parser.add_argument("--filename", type=str, required=False,
help="filename for saving the JSON-formatted AccountInfo data")
parser.add_argument(
"--address", type=PublicKey, required=True, help="address of the account"
)
parser.add_argument(
"--filename",
type=str,
required=False,
help="filename for saving the JSON-formatted AccountInfo data",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)

View File

@ -9,17 +9,24 @@ import typing
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Display the balances of all group tokens in the current wallet.")
parser = argparse.ArgumentParser(
description="Display the balances of all group tokens in the current wallet."
)
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--address", type=PublicKey,
help="Root address to check (if not provided, the wallet address is used)")
parser.add_argument("--json-filename", type=str,
help="If specified, a file to write the balance information in JSON format")
parser.add_argument(
"--address",
type=PublicKey,
help="Root address to check (if not provided, the wallet address is used)",
)
parser.add_argument(
"--json-filename",
type=str,
help="If specified, a file to write the balance information in JSON format",
)
args: argparse.Namespace = mango.parse_args(parser)
address: typing.Optional[PublicKey] = args.address

View File

@ -7,15 +7,18 @@ import sys
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Shows details of a Mango account.")
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--address", type=PublicKey, required=False,
help="address of the owner of the account (defaults to the root address of the wallet)")
parser.add_argument(
"--address",
type=PublicKey,
required=False,
help="address of the owner of the account (defaults to the root address of the wallet)",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)

View File

@ -8,23 +8,37 @@ import typing
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Shows the on-chain data of a particular account.")
parser = argparse.ArgumentParser(
description="Shows the on-chain data of a particular account."
)
mango.ContextBuilder.add_command_line_parameters(parser)
parser.add_argument("--address", type=PublicKey, required=True, help="Address of the Solana account to watch")
parser.add_argument("--account-type", type=str, default="AccountInfo",
help="Underlying object type of the data in the AccountInfo")
parser.add_argument(
"--address",
type=PublicKey,
required=True,
help="Address of the Solana account to watch",
)
parser.add_argument(
"--account-type",
type=str,
default="AccountInfo",
help="Underlying object type of the data in the AccountInfo",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
converter: typing.Callable[[mango.AccountInfo], typing.Any] = lambda account_info: account_info
converter: typing.Callable[
[mango.AccountInfo], typing.Any
] = lambda account_info: account_info
if args.account_type.upper() != "ACCOUNTINFO":
converter = mango.build_account_info_converter(context, args.account_type)
account_info: typing.Optional[mango.AccountInfo] = mango.AccountInfo.load(context, args.address)
account_info: typing.Optional[mango.AccountInfo] = mango.AccountInfo.load(
context, args.address
)
if account_info is None:
raise Exception(f"No account found at address: {args.address}")

View File

@ -7,15 +7,18 @@ import sys
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Shows details of a Mango account.")
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--address", type=PublicKey, required=False,
help="address of the delegate of the account (defaults to the root address of the wallet)")
parser.add_argument(
"--address",
type=PublicKey,
required=False,
help="address of the delegate of the account (defaults to the root address of the wallet)",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)

View File

@ -6,20 +6,31 @@ import os.path
import sys
import typing
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Shows the on-chain data of a particular account.")
parser = argparse.ArgumentParser(
description="Shows the on-chain data of a particular account."
)
mango.ContextBuilder.add_command_line_parameters(parser)
parser.add_argument("--filename", type=str, required=False,
help="filename for loading the JSON-formatted AccountInfo data")
parser.add_argument("--account-type", type=str, default="AccountInfo",
help="Underlying object type of the data in the AccountInfo")
parser.add_argument(
"--filename",
type=str,
required=False,
help="filename for loading the JSON-formatted AccountInfo data",
)
parser.add_argument(
"--account-type",
type=str,
default="AccountInfo",
help="Underlying object type of the data in the AccountInfo",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
converter: typing.Callable[[mango.AccountInfo], typing.Any] = lambda account_info: account_info
converter: typing.Callable[
[mango.AccountInfo], typing.Any
] = lambda account_info: account_info
if args.account_type.upper() != "ACCOUNTINFO":
converter = mango.build_account_info_converter(context, args.account_type)

View File

@ -6,14 +6,19 @@ import os.path
import sys
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(
description="Shows the current funding rates for a perp market in a Mango Markets Group.")
description="Shows the current funding rates for a perp market in a Mango Markets Group."
)
mango.ContextBuilder.add_command_line_parameters(parser)
parser.add_argument("--market", type=str, required=True, help="symbol of the market to look up, e.g. 'ETH-PERP'")
parser.add_argument(
"--market",
type=str,
required=True,
help="symbol of the market to look up, e.g. 'ETH-PERP'",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)

View File

@ -7,11 +7,12 @@ import os.path
import sys
import traceback
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
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.ContextBuilder.add_command_line_parameters(parser)
args: argparse.Namespace = mango.parse_args(parser)
@ -21,6 +22,10 @@ try:
group = mango.Group.load(context)
mango.output(group)
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()}"
)
except:
logging.critical(f"show-group stopped because of uncatchable error: {traceback.format_exc()}")
logging.critical(
f"show-group stopped because of uncatchable error: {traceback.format_exc()}"
)

View File

@ -5,11 +5,12 @@ import os
import os.path
import sys
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
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.ContextBuilder.add_command_line_parameters(parser)
args: argparse.Namespace = mango.parse_args(parser)
@ -23,4 +24,6 @@ for slot in group.slots:
if slot.base_instrument is not None:
price = group.token_price_from_cache(cache, slot.base_instrument)
price_formatted = f"{price.value:,.8f}"
mango.output(f"{slot.base_instrument.symbol:<6}: {price_formatted:>18} {group.shared_quote_token.symbol}")
mango.output(
f"{slot.base_instrument.symbol:<6}: {price_formatted:>18} {group.shared_quote_token.symbol}"
)

View File

@ -8,14 +8,15 @@ import sys
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Shows health of a Mango account.")
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--address", type=PublicKey, required=False, help="address of the Mango account")
parser.add_argument(
"--address", type=PublicKey, required=False, help="address of the Mango account"
)
args: argparse.Namespace = mango.parse_args(parser)
context: mango.Context = mango.ContextBuilder.from_command_line_parameters(args)
@ -33,19 +34,36 @@ else:
mango_account = mango.Account.load(context, address, group)
health_calculator = mango.calculators.healthcalculator.HealthCalculator(
context, mango.calculators.healthcalculator.HealthType.INITIAL)
context, mango.calculators.healthcalculator.HealthType.INITIAL
)
spot_open_orders_addresses = list(
[basket_token.spot_open_orders for basket_token in mango_account.slots if basket_token.spot_open_orders is not None])
spot_open_orders_account_infos = mango.AccountInfo.load_multiple(context, spot_open_orders_addresses)
[
basket_token.spot_open_orders
for basket_token in mango_account.slots
if basket_token.spot_open_orders is not None
]
)
spot_open_orders_account_infos = mango.AccountInfo.load_multiple(
context, spot_open_orders_addresses
)
spot_open_orders_account_infos_by_address = {
str(account_info.address): account_info for account_info in spot_open_orders_account_infos}
str(account_info.address): account_info
for account_info in spot_open_orders_account_infos
}
spot_open_orders = {}
for basket_token in mango_account.slots:
if basket_token.spot_open_orders is not None:
account_info = spot_open_orders_account_infos_by_address[str(basket_token.spot_open_orders)]
oo = mango.OpenOrders.parse(account_info, basket_token.base_instrument.decimals,
mango_account.shared_quote_token.decimals)
account_info = spot_open_orders_account_infos_by_address[
str(basket_token.spot_open_orders)
]
oo = mango.OpenOrders.parse(
account_info,
basket_token.base_instrument.decimals,
mango_account.shared_quote_token.decimals,
)
spot_open_orders[str(basket_token.spot_open_orders)] = oo
mango.output("Health", health_calculator.calculate(mango_account, spot_open_orders, group, cache))
mango.output(
"Health", health_calculator.calculate(mango_account, spot_open_orders, group, cache)
)

View File

@ -6,13 +6,19 @@ import os.path
import sys
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Shows the current interest rates for a token in a Mango Markets Group.")
parser = argparse.ArgumentParser(
description="Shows the current interest rates for a token in a Mango Markets Group."
)
mango.ContextBuilder.add_command_line_parameters(parser)
parser.add_argument("--symbol", type=str, required=True, help="symbol of the token to look up, e.g. 'ETH'")
parser.add_argument(
"--symbol",
type=str,
required=True,
help="symbol of the token to look up, e.g. 'ETH'",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)

View File

@ -5,13 +5,19 @@ import os
import os.path
import sys
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Shows the on-chain data of a particular account.")
parser = argparse.ArgumentParser(
description="Shows the on-chain data of a particular account."
)
mango.ContextBuilder.add_command_line_parameters(parser)
parser.add_argument("--market", type=str, required=True, help="perp market symbol to inspect (e.g. SOL-PERP)")
parser.add_argument(
"--market",
type=str,
required=True,
help="perp market symbol to inspect (e.g. SOL-PERP)",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)

View File

@ -5,13 +5,14 @@ import os
import os.path
import sys
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Shows all properties of a given market.")
mango.ContextBuilder.add_command_line_parameters(parser)
parser.add_argument("--market", type=str, required=True, help="market symbol to buy (e.g. ETH/USDC)")
parser.add_argument(
"--market", type=str, required=True, help="market symbol to buy (e.g. ETH/USDC)"
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)

View File

@ -7,25 +7,38 @@ import sys
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
import mango.marketmaking # nopep8
parser = argparse.ArgumentParser(description="Shows all properties of a given market.")
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--market", type=str, required=True, help="market symbol load model state for (e.g. ETH/USDC)")
parser.add_argument("--oracle-provider", type=str, required=True,
help="name of the price provider to use (e.g. pyth-mainnet)")
parser.add_argument("--account-address", type=PublicKey,
help="address of the specific account to use, if more than one available")
parser.add_argument(
"--market",
type=str,
required=True,
help="market symbol load model state for (e.g. ETH/USDC)",
)
parser.add_argument(
"--oracle-provider",
type=str,
required=True,
help="name of the price provider to use (e.g. pyth-mainnet)",
)
parser.add_argument(
"--account-address",
type=PublicKey,
help="address of the specific account to use, if more than one available",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
group = mango.Group.load(context, context.group_address)
account = mango.Account.load_for_owner_by_address(context, wallet.address, group, args.account_address)
account = mango.Account.load_for_owner_by_address(
context, wallet.address, group, args.account_address
)
market = context.market_lookup.find_by_symbol(args.market)
if market is None:
@ -33,17 +46,33 @@ if market is None:
market = mango.ensure_market_loaded(context, market)
oracle_provider: mango.OracleProvider = mango.create_oracle_provider(context, args.oracle_provider)
oracle_provider: mango.OracleProvider = mango.create_oracle_provider(
context, args.oracle_provider
)
oracle = oracle_provider.oracle_for_market(context, market)
if oracle is None:
raise Exception(f"Could not find oracle for market {market.symbol} from provider {args.oracle_provider}.")
raise Exception(
f"Could not find oracle for market {market.symbol} from provider {args.oracle_provider}."
)
disposer = mango.DisposePropagator()
health_check = mango.HealthCheck()
disposer.add_disposable(health_check)
manager = mango.IndividualWebSocketSubscriptionManager(context) # Should never be used
model_state_builder: mango.marketmaking.ModelStateBuilder = mango.marketmaking.model_state_builder_factory(
mango.marketmaking.ModelUpdateMode.POLL, context, disposer, manager, health_check, wallet, group, account, market, oracle)
model_state_builder: mango.marketmaking.ModelStateBuilder = (
mango.marketmaking.model_state_builder_factory(
mango.marketmaking.ModelUpdateMode.POLL,
context,
disposer,
manager,
health_check,
wallet,
group,
account,
market,
oracle,
)
)
model_state = model_state_builder.build(context)
mango.output(model_state)

View File

@ -7,30 +7,44 @@ import sys
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Shows all orders on the given market owned by the current wallet.")
parser = argparse.ArgumentParser(
description="Shows all orders on the given market owned by the current wallet."
)
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--market", type=str, required=True, help="market symbol to buy (e.g. ETH/USDC)")
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(
"--market", type=str, required=True, help="market symbol to buy (e.g. ETH/USDC)"
)
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",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
group = mango.Group.load(context, context.group_address)
account = mango.Account.load_for_owner_by_address(context, wallet.address, group, args.account_address)
account = mango.Account.load_for_owner_by_address(
context, wallet.address, group, args.account_address
)
market = context.market_lookup.find_by_symbol(args.market)
if market is None:
raise Exception(f"Could not find market {args.market}")
market_operations = mango.create_market_operations(context, wallet, account, market, args.dry_run)
market_operations = mango.create_market_operations(
context, wallet, account, market, args.dry_run
)
orders = market_operations.load_my_orders()
mango.output(f"{len(orders)} order(s) to show.")
for order in orders:

View File

@ -8,17 +8,22 @@ import typing
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), '..')))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Shows Mango open orders accounts.")
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--address", type=PublicKey,
help="Root address to check (if not provided, the wallet address is used)")
parser.add_argument("--account-address", type=PublicKey,
help="address of the specific account to use, if more than one available")
parser.add_argument(
"--address",
type=PublicKey,
help="Root address to check (if not provided, the wallet address is used)",
)
parser.add_argument(
"--account-address",
type=PublicKey,
help="address of the specific account to use, if more than one available",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
@ -28,16 +33,24 @@ if address is None:
address = wallet.address
group = mango.Group.load(context)
account = mango.Account.load_for_owner_by_address(context, wallet.address, group, args.account_address)
account = mango.Account.load_for_owner_by_address(
context, wallet.address, group, args.account_address
)
at_least_one_open_orders_account = False
quote_token_bank = group.shared_quote_token
for slot in account.slots:
if slot.spot_open_orders is not None:
if slot.base_token_bank is None:
raise Exception(f"No base token available for token {slot.base_instrument}.")
open_orders = mango.OpenOrders.load(context, slot.spot_open_orders,
slot.base_token_bank.token.decimals, slot.quote_token_bank.token.decimals)
raise Exception(
f"No base token available for token {slot.base_instrument}."
)
open_orders = mango.OpenOrders.load(
context,
slot.spot_open_orders,
slot.base_token_bank.token.decimals,
slot.quote_token_bank.token.decimals,
)
mango.output(slot.base_instrument)
mango.output(open_orders)
at_least_one_open_orders_account = True

View File

@ -7,24 +7,34 @@ import sys
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Shows all orders on a market.")
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--market", type=str, required=True, help="market symbol to buy (e.g. ETH/USDC)")
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(
"--market", type=str, required=True, help="market symbol to buy (e.g. ETH/USDC)"
)
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",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
group = mango.Group.load(context, context.group_address)
account = mango.Account.load_for_owner_by_address(context, wallet.address, group, args.account_address)
account = mango.Account.load_for_owner_by_address(
context, wallet.address, group, args.account_address
)
market = mango.load_market_by_symbol(context, args.market)

View File

@ -6,25 +6,37 @@ import os
import os.path
import sys
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Displays the price from the Pyth Network.")
parser = argparse.ArgumentParser(
description="Displays the price from the Pyth Network."
)
mango.ContextBuilder.add_command_line_parameters(parser)
parser.add_argument("--provider", type=str, required=True,
help="name of the price provider to use (e.g. pyth)")
parser.add_argument("--market", type=str, required=True,
help="market symbol to display (e.g. ETH/USDC)")
parser.add_argument("--stream", action="store_true", default=False,
help="stream the prices until stopped")
parser.add_argument(
"--provider",
type=str,
required=True,
help="name of the price provider to use (e.g. pyth)",
)
parser.add_argument(
"--market", type=str, required=True, help="market symbol to display (e.g. ETH/USDC)"
)
parser.add_argument(
"--stream",
action="store_true",
default=False,
help="stream the prices until stopped",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
logging.info(str(context))
oracle_provider: mango.OracleProvider = mango.create_oracle_provider(context, args.provider)
oracle_provider: mango.OracleProvider = mango.create_oracle_provider(
context, args.provider
)
market = context.market_lookup.find_by_symbol(args.market)
if market is None:
@ -32,7 +44,9 @@ if market is None:
oracle = oracle_provider.oracle_for_market(context, market)
if oracle is None:
mango.output(f"Could not find oracle for market {market.symbol} from provider {args.provider}.")
mango.output(
f"Could not find oracle for market {market.symbol} from provider {args.provider}."
)
else:
if not args.stream:
price = oracle.fetch_price(context)
@ -40,7 +54,9 @@ else:
else:
mango.output("Press <ENTER> to quit.")
price_subscription = oracle.to_streaming_observable(context)
disposable = price_subscription.subscribe(mango.PrintingObserverSubscriber(False))
disposable = price_subscription.subscribe(
mango.PrintingObserverSubscriber(False)
)
# Wait - don't exit
input()

View File

@ -8,16 +8,20 @@ import typing
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), '..')))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Shows Mango open orders accounts.")
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--market", type=str, required=True, help="market symbol (e.g. ETH/USDC)")
parser.add_argument("--address", type=PublicKey,
help="Root address to check (if not provided, the wallet address is used)")
parser.add_argument(
"--market", type=str, required=True, help="market symbol (e.g. ETH/USDC)"
)
parser.add_argument(
"--address",
type=PublicKey,
help="Root address to check (if not provided, the wallet address is used)",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
@ -35,7 +39,15 @@ if not isinstance(market, mango.SerumMarket):
raise Exception(f"Market {args.market} is not a Serum market: {market}")
all_open_orders_for_market = mango.OpenOrders.load_for_market_and_owner(
context, market.address, address, context.serum_program_address, market.base.decimals, market.quote.decimals)
mango.output(f"Found {len(all_open_orders_for_market)} Serum OpenOrders account(s) for market {market.symbol}.")
context,
market.address,
address,
context.serum_program_address,
market.base.decimals,
market.quote.decimals,
)
mango.output(
f"Found {len(all_open_orders_for_market)} Serum OpenOrders account(s) for market {market.symbol}."
)
for open_orders in all_open_orders_for_market:
mango.output(open_orders)

View File

@ -8,21 +8,30 @@ import typing
from decimal import Decimal
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Shows all Wrapped SOL accounts for the wallet.")
parser = argparse.ArgumentParser(
description="Shows all Wrapped SOL accounts for the wallet."
)
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--symbol", type=str, required=True,
help="symbol of the token to look up, e.g. 'ETH'")
parser.add_argument("--owner", type=PublicKey,
help="wallet address of the wallet owner")
parser.add_argument("--mint", type=PublicKey,
help="mint address of the token")
parser.add_argument("--decimals", type=Decimal, default=Decimal(6),
help="number of decimal places for token values")
parser.add_argument(
"--symbol",
type=str,
required=True,
help="symbol of the token to look up, e.g. 'ETH'",
)
parser.add_argument(
"--owner", type=PublicKey, help="wallet address of the wallet owner"
)
parser.add_argument("--mint", type=PublicKey, help="mint address of the token")
parser.add_argument(
"--decimals",
type=Decimal,
default=Decimal(6),
help="number of decimal places for token values",
)
args: argparse.Namespace = mango.parse_args(parser)
context: mango.Context = mango.ContextBuilder.from_command_line_parameters(args)
@ -33,10 +42,13 @@ token: mango.Token
if args.mint is not None:
token = mango.Token(args.symbol, args.symbol, args.decimals, args.mint)
else:
instrument: mango.Instrument = context.instrument_lookup.find_by_symbol_or_raise(args.symbol)
instrument: mango.Instrument = context.instrument_lookup.find_by_symbol_or_raise(
args.symbol
)
token = mango.Token.ensure(instrument)
token_accounts: typing.Sequence[mango.TokenAccount] = mango.TokenAccount.fetch_all_for_owner_and_token(
context, owner_address, token)
token_accounts: typing.Sequence[
mango.TokenAccount
] = mango.TokenAccount.fetch_all_for_owner_and_token(context, owner_address, token)
if len(token_accounts) == 0:
mango.output(f"No token accounts for {token}.")

View File

@ -5,13 +5,16 @@ import os
import os.path
import sys
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Shows the on-chain data of a particular transaction.")
parser = argparse.ArgumentParser(
description="Shows the on-chain data of a particular transaction."
)
mango.ContextBuilder.add_command_line_parameters(parser)
parser.add_argument("--signature", type=str, required=True, help="signature of the transaction")
parser.add_argument(
"--signature", type=str, required=True, help="signature of the transaction"
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)

View File

@ -5,14 +5,16 @@ import os
import os.path
import sys
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(
description="Shows the on-chain logs of a particular transaction (decoding mango-log data).")
description="Shows the on-chain logs of a particular transaction (decoding mango-log data)."
)
mango.ContextBuilder.add_command_line_parameters(parser)
parser.add_argument("--signature", type=str, required=True, help="signature of the transaction")
parser.add_argument(
"--signature", type=str, required=True, help="signature of the transaction"
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)

View File

@ -4,11 +4,12 @@ import argparse
import os
import sys
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Shows all Wrapped SOL accounts for the wallet.")
parser = argparse.ArgumentParser(
description="Shows all Wrapped SOL accounts for the wallet."
)
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
args: argparse.Namespace = mango.parse_args(parser)
@ -16,9 +17,13 @@ args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
wrapped_sol: mango.Token = mango.Token.ensure(context.instrument_lookup.find_by_symbol_or_raise("SOL"))
wrapped_sol: mango.Token = mango.Token.ensure(
context.instrument_lookup.find_by_symbol_or_raise("SOL")
)
token_accounts = mango.TokenAccount.fetch_all_for_owner_and_token(context, wallet.address, wrapped_sol)
token_accounts = mango.TokenAccount.fetch_all_for_owner_and_token(
context, wallet.address, wrapped_sol
)
if len(token_accounts) == 0:
mango.output("No wrapped SOL accounts.")

View File

@ -13,36 +13,66 @@ from decimal import Decimal
from solana.publickey import PublicKey
from threading import Thread
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
import mango.simplemarketmaking.simplemarketmaker # nopep8
parser = argparse.ArgumentParser(description="Runs a simple market-maker.")
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--market", type=str, required=True, help="market symbol to buy (e.g. ETH/USDC)")
parser.add_argument("--spread-ratio", type=Decimal, required=True,
help="fraction of the mid price to be added and subtracted to calculate buy and sell prices")
parser.add_argument("--position-size-ratio", type=Decimal, required=True,
help="fraction of the token inventory to be bought or sold in each order")
parser.add_argument("--existing-order-tolerance", type=Decimal, default=Decimal("0.001"),
help="fraction of the token inventory to be bought or sold in each order")
parser.add_argument("--pause-duration", type=int, default=10,
help="number of seconds to pause between placing orders and cancelling them")
parser.add_argument("--oracle-provider", type=str, default="serum",
help="name of the oracle service providing the prices")
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(
"--market", type=str, required=True, help="market symbol to buy (e.g. ETH/USDC)"
)
parser.add_argument(
"--spread-ratio",
type=Decimal,
required=True,
help="fraction of the mid price to be added and subtracted to calculate buy and sell prices",
)
parser.add_argument(
"--position-size-ratio",
type=Decimal,
required=True,
help="fraction of the token inventory to be bought or sold in each order",
)
parser.add_argument(
"--existing-order-tolerance",
type=Decimal,
default=Decimal("0.001"),
help="fraction of the token inventory to be bought or sold in each order",
)
parser.add_argument(
"--pause-duration",
type=int,
default=10,
help="number of seconds to pause between placing orders and cancelling them",
)
parser.add_argument(
"--oracle-provider",
type=str,
default="serum",
help="name of the oracle service providing the prices",
)
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",
)
args: argparse.Namespace = mango.parse_args(parser)
try:
context = mango.ContextBuilder.from_command_line_parameters(args)
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
group = mango.Group.load(context, context.group_address)
account = mango.Account.load_for_owner_by_address(context, wallet.address, group, args.account_address)
account = mango.Account.load_for_owner_by_address(
context, wallet.address, group, args.account_address
)
market_stub = context.market_lookup.find_by_symbol(args.market)
if market_stub is None:
@ -52,16 +82,28 @@ try:
raise Exception(f"Market is not a serum market: {market}")
market_operations: mango.MarketOperations = mango.create_market_operations(
context, wallet, account, market, args.dry_run)
context, wallet, account, market, args.dry_run
)
oracle_provider: mango.OracleProvider = mango.create_oracle_provider(context, args.oracle_provider)
oracle_provider: mango.OracleProvider = mango.create_oracle_provider(
context, args.oracle_provider
)
oracle = oracle_provider.oracle_for_market(context, market)
if oracle is None:
raise Exception(f"Could not find oracle for spot market {args.market}")
pause_duration = timedelta(seconds=args.pause_duration)
market_maker = mango.simplemarketmaking.simplemarketmaker.SimpleMarketMaker(
context, wallet, market, market_operations, oracle, args.spread_ratio, args.position_size_ratio, args.existing_order_tolerance, pause_duration)
context,
wallet,
market,
market_operations,
oracle,
args.spread_ratio,
args.position_size_ratio,
args.existing_order_tolerance,
pause_duration,
)
mango.output(f"Starting {market_maker} - use <Enter> to stop.")
thread = Thread(target=market_maker.start)
@ -77,6 +119,10 @@ try:
mango.output(f"Stopping {market_maker} on next iteration...")
market_maker.stop()
except Exception as exception:
logging.critical(f"Market maker stopped because of exception: {exception} - {traceback.format_exc()}")
logging.critical(
f"Market maker stopped because of exception: {exception} - {traceback.format_exc()}"
)
except:
logging.critical(f"Market maker stopped because of uncatchable error: {traceback.format_exc()}")
logging.critical(
f"Market maker stopped because of uncatchable error: {traceback.format_exc()}"
)

View File

@ -7,17 +7,21 @@ import os.path
import sys
import traceback
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
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="Run the Transaction Scout to display information about a specific transaction.")
description="Run the Transaction Scout to display information about a specific transaction."
)
mango.ContextBuilder.add_command_line_parameters(parser)
parser.add_argument("--signature", type=str, required=True,
help="The signature of the transaction to look up")
parser.add_argument(
"--signature",
type=str,
required=True,
help="The signature of the transaction to look up",
)
args: argparse.Namespace = mango.parse_args(parser)
try:
@ -30,6 +34,10 @@ try:
report = mango.TransactionScout.load(context, signature)
mango.output(report)
except Exception as exception:
logging.critical(f"transaction-scout stopped because of exception: {exception} - {traceback.format_exc()}")
logging.critical(
f"transaction-scout stopped because of exception: {exception} - {traceback.format_exc()}"
)
except:
logging.critical(f"transaction-scout stopped because of uncatchable error: {traceback.format_exc()}")
logging.critical(
f"transaction-scout stopped because of uncatchable error: {traceback.format_exc()}"
)

View File

@ -6,35 +6,55 @@ import sys
from decimal import Decimal
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Unwraps Wrapped SOL to Pure SOL and adds it to the wallet account.")
parser = argparse.ArgumentParser(
description="Unwraps Wrapped SOL to Pure SOL and adds it to the wallet account."
)
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--quantity", type=Decimal, required=True, help="quantity of SOL to unwrap")
parser.add_argument(
"--quantity", type=Decimal, required=True, help="quantity of SOL to unwrap"
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
wrapped_sol: mango.Token = mango.Token.ensure(context.instrument_lookup.find_by_symbol_or_raise("SOL"))
wrapped_sol: mango.Token = mango.Token.ensure(
context.instrument_lookup.find_by_symbol_or_raise("SOL")
)
largest_token_account = mango.TokenAccount.fetch_largest_for_owner_and_token(
context, wallet.address, wrapped_sol)
context, wallet.address, wrapped_sol
)
if largest_token_account is None:
raise Exception(f"No {wrapped_sol.name} accounts found for owner {wallet.address}.")
signers: mango.CombinableInstructions = mango.CombinableInstructions.from_signers([wallet.keypair])
create_instructions = mango.build_create_spl_account_instructions(context, wallet, wrapped_sol)
signers: mango.CombinableInstructions = mango.CombinableInstructions.from_signers(
[wallet.keypair]
)
create_instructions = mango.build_create_spl_account_instructions(
context, wallet, wrapped_sol
)
wrapped_sol_address = create_instructions.signers[0].public_key
unwrap_instructions = mango.build_transfer_spl_tokens_instructions(
context, wallet, wrapped_sol, largest_token_account.address, wrapped_sol_address, args.quantity)
close_instructions = mango.build_close_spl_account_instructions(context, wallet, wrapped_sol_address)
context,
wallet,
wrapped_sol,
largest_token_account.address,
wrapped_sol_address,
args.quantity,
)
close_instructions = mango.build_close_spl_account_instructions(
context, wallet, wrapped_sol_address
)
all_instructions = signers + create_instructions + unwrap_instructions + close_instructions
all_instructions = (
signers + create_instructions + unwrap_instructions + close_instructions
)
mango.output("Unwrapping SOL:")
mango.output(f" Temporary account: {wrapped_sol_address}")

View File

@ -11,16 +11,26 @@ import threading
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Shows the on-chain data of a particular account.")
parser = argparse.ArgumentParser(
description="Shows the on-chain data of a particular account."
)
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--address", type=PublicKey, required=True, help="Address of the Solana account to watch")
parser.add_argument("--account-type", type=str, required=True,
help="Underlying object type of the data in the AccountInfo")
parser.add_argument(
"--address",
type=PublicKey,
required=True,
help="Address of the Solana account to watch",
)
parser.add_argument(
"--account-type",
type=str,
required=True,
help="Underlying object type of the data in the AccountInfo",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
@ -31,31 +41,51 @@ disposer.add_disposable(manager)
if args.account_type.upper() == "ACCOUNTINFO":
raw_subscription = mango.WebSocketAccountSubscription(
context, args.address, lambda account_info: account_info)
context, args.address, lambda account_info: account_info
)
manager.add(raw_subscription)
publisher: rx.core.typing.Observable[mango.AccountInfo] = raw_subscription.publisher
elif args.account_type.upper() == "SERUMEVENTS":
initial_serum_event_queue: mango.SerumEventQueue = mango.SerumEventQueue.load(context, args.address)
serum_splitter: mango.UnseenSerumEventChangesTracker = mango.UnseenSerumEventChangesTracker(
initial_serum_event_queue)
initial_serum_event_queue: mango.SerumEventQueue = mango.SerumEventQueue.load(
context, args.address
)
serum_splitter: mango.UnseenSerumEventChangesTracker = (
mango.UnseenSerumEventChangesTracker(initial_serum_event_queue)
)
serum_event_splitting_subscription = mango.WebSocketAccountSubscription(
context, args.address, lambda account_info: mango.SerumEventQueue.parse(account_info))
context,
args.address,
lambda account_info: mango.SerumEventQueue.parse(account_info),
)
manager.add(serum_event_splitting_subscription)
publisher = serum_event_splitting_subscription.publisher.pipe(rx.operators.flat_map(serum_splitter.unseen))
publisher = serum_event_splitting_subscription.publisher.pipe(
rx.operators.flat_map(serum_splitter.unseen)
)
elif args.account_type.upper() == "PERPEVENTS":
# It'd be nice to get the market's lot size converter, but we don't have its address yet.
lot_size_converter: mango.LotSizeConverter = mango.NullLotSizeConverter()
initial_perp_event_queue: mango.PerpEventQueue = mango.PerpEventQueue.load(
context, args.address, lot_size_converter)
perp_splitter: mango.UnseenPerpEventChangesTracker = mango.UnseenPerpEventChangesTracker(initial_perp_event_queue)
context, args.address, lot_size_converter
)
perp_splitter: mango.UnseenPerpEventChangesTracker = (
mango.UnseenPerpEventChangesTracker(initial_perp_event_queue)
)
perp_event_splitting_subscription = mango.WebSocketAccountSubscription(
context, args.address, lambda account_info: mango.PerpEventQueue.parse(account_info, lot_size_converter))
context,
args.address,
lambda account_info: mango.PerpEventQueue.parse(
account_info, lot_size_converter
),
)
manager.add(perp_event_splitting_subscription)
publisher = perp_event_splitting_subscription.publisher.pipe(rx.operators.flat_map(perp_splitter.unseen))
publisher = perp_event_splitting_subscription.publisher.pipe(
rx.operators.flat_map(perp_splitter.unseen)
)
else:
converter = mango.build_account_info_converter(context, args.account_type)
converting_subscription = mango.WebSocketAccountSubscription(
context, args.address, converter)
context, args.address, converter
)
manager.add(converting_subscription)
publisher = converting_subscription.publisher

View File

@ -13,21 +13,43 @@ import threading
from solana.publickey import PublicKey
from solana.rpc.commitment import Max
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Show program logs for an account, as they arrive.")
parser = argparse.ArgumentParser(
description="Show program logs for an account, as they arrive."
)
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--address", type=PublicKey, action="append", default=[], required=True,
help="Address of the Solana account to watch (can be specified multiple times)")
parser.add_argument("--notify", type=mango.parse_notification_target, action="append", default=[],
help="The notification target for all liquidation events")
parser.add_argument("--notify-successful", type=mango.parse_notification_target,
action="append", default=[], help="The notification target for successful liquidations")
parser.add_argument("--notify-failed", type=mango.parse_notification_target,
action="append", default=[], help="The notification target for failed liquidations")
parser.add_argument(
"--address",
type=PublicKey,
action="append",
default=[],
required=True,
help="Address of the Solana account to watch (can be specified multiple times)",
)
parser.add_argument(
"--notify",
type=mango.parse_notification_target,
action="append",
default=[],
help="The notification target for all liquidation events",
)
parser.add_argument(
"--notify-successful",
type=mango.parse_notification_target,
action="append",
default=[],
help="The notification target for successful liquidations",
)
parser.add_argument(
"--notify-failed",
type=mango.parse_notification_target,
action="append",
default=[],
help="The notification target for failed liquidations",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
@ -54,10 +76,12 @@ publisher.pipe(
# rx.operators.map(mango.debug_print_item("Transaction")),
# rx.operators.delay(30), # Wait for the transaction to be fully confirmed
# rx.operators.map(mango.debug_print_item("After Delay")),
rx.operators.map(lambda log_event: mango.TransactionScout.load(context, log_event.signatures[0])),
rx.operators.map(
lambda log_event: mango.TransactionScout.load(context, log_event.signatures[0])
),
rx.operators.filter(lambda item: item is not None),
rx.operators.catch(mango.observable_pipeline_error_reporter),
rx.operators.retry()
rx.operators.retry(),
).subscribe(mango.PrintingObserverSubscriber(False))
manager.open()

View File

@ -14,35 +14,62 @@ import typing
from decimal import Decimal
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(
description="Watches one or many accounts (via a websocket) and sends a notification if the SOL balance falls below the --minimum-sol-balance threshold.")
description="Watches one or many accounts (via a websocket) and sends a notification if the SOL balance falls below the --minimum-sol-balance threshold."
)
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--named-address", type=str, required=True, action="append", default=[],
help="Name and address of the Solana account to watch, separated by a colon")
parser.add_argument("--minimum-sol-balance", type=Decimal, default=Decimal("0.1"),
help="the minimum SOL balance required for the alert. A SOL balance less than this value will trigger a nofitication.")
parser.add_argument("--timer-limit", type=int, default=(60 * 60),
help="notifications for an account will be sent at most once per timer-limit seconds, and accounts will be polled once per timer-limit seconds irrespective of websocket activity")
parser.add_argument("--notify", type=mango.parse_notification_target, action="append", default=[],
help="The notification target for low balance events")
parser.add_argument("--notify-events", type=mango.parse_notification_target, action="append", default=[],
help="The notification target for startup events")
parser.add_argument(
"--named-address",
type=str,
required=True,
action="append",
default=[],
help="Name and address of the Solana account to watch, separated by a colon",
)
parser.add_argument(
"--minimum-sol-balance",
type=Decimal,
default=Decimal("0.1"),
help="the minimum SOL balance required for the alert. A SOL balance less than this value will trigger a nofitication.",
)
parser.add_argument(
"--timer-limit",
type=int,
default=(60 * 60),
help="notifications for an account will be sent at most once per timer-limit seconds, and accounts will be polled once per timer-limit seconds irrespective of websocket activity",
)
parser.add_argument(
"--notify",
type=mango.parse_notification_target,
action="append",
default=[],
help="The notification target for low balance events",
)
parser.add_argument(
"--notify-events",
type=mango.parse_notification_target,
action="append",
default=[],
help="The notification target for startup events",
)
args: argparse.Namespace = mango.parse_args(parser)
notify_balance: mango.NotificationTarget = mango.CompoundNotificationTarget(args.notify)
notify_event: mango.NotificationTarget = mango.CompoundNotificationTarget(args.notify_events)
notify_event: mango.NotificationTarget = mango.CompoundNotificationTarget(
args.notify_events
)
def notifier(name: str) -> typing.Callable[[mango.AccountInfo], None]:
def notify(account_info: mango.AccountInfo) -> None:
report = f"Account \"{name} [{account_info.address}]\" on {context.client.cluster_name} has only {account_info.sols} SOL, which is below the minimum required balance of {args.minimum_sol_balance} SOL."
report = f'Account "{name} [{account_info.address}]" on {context.client.cluster_name} has only {account_info.sols} SOL, which is below the minimum required balance of {args.minimum_sol_balance} SOL.'
notify_balance.send(f"[{args.name}] {report}")
mango.output(f"Notification sent: {report}")
return notify
@ -55,7 +82,13 @@ def log_account(account_info: mango.AccountInfo) -> mango.AccountInfo:
return account_info
def add_subscription_for_parameter(context: mango.Context, manager: mango.WebSocketSubscriptionManager, health_check: mango.HealthCheck, timer_limit: int, name_and_address: str) -> None:
def add_subscription_for_parameter(
context: mango.Context,
manager: mango.WebSocketSubscriptionManager,
health_check: mango.HealthCheck,
timer_limit: int,
name_and_address: str,
) -> None:
name, address_str = name_and_address.split(":")
address = PublicKey(address_str)
@ -63,19 +96,22 @@ def add_subscription_for_parameter(context: mango.Context, manager: mango.WebSoc
if immediate is None:
raise Exception(f"No account '{name}' at {address_str}.")
account_subscription = mango.WebSocketAccountSubscription(context, address, lambda account_info: account_info)
account_subscription = mango.WebSocketAccountSubscription(
context, address, lambda account_info: account_info
)
manager.add(account_subscription)
on_change = account_subscription.publisher.pipe(rx.operators.start_with(immediate))
on_timer = rx.interval(timer_limit).pipe(
rx.operators.map(lambda _: mango.AccountInfo.load(context, address)))
rx.operators.map(lambda _: mango.AccountInfo.load(context, address))
)
rx.merge(on_change, on_timer).pipe(
rx.operators.observe_on(context.create_thread_pool_scheduler()),
rx.operators.map(log_account),
rx.operators.filter(account_fails_balance_check),
rx.operators.throttle_first(timer_limit),
rx.operators.catch(mango.observable_pipeline_error_reporter),
rx.operators.retry()
rx.operators.retry(),
).subscribe(notifier(name))
@ -91,7 +127,9 @@ disposer.add_disposable(health_check)
health_check.add("ws_pong", manager.pong)
for name_and_address in args.named_address:
add_subscription_for_parameter(context, manager, health_check, args.timer_limit, name_and_address)
add_subscription_for_parameter(
context, manager, health_check, args.timer_limit, name_and_address
)
manager.open()

View File

@ -5,8 +5,7 @@ import os
import os.path
import sys
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), '..')))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="shows the address of the current wallet")

View File

@ -8,36 +8,54 @@ import sys
from decimal import Decimal
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), '..')))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(description="Withdraw funds from a Mango account")
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--symbol", type=str, required=True, help="token symbol to withdraw (e.g. USDC)")
parser.add_argument("--quantity", type=Decimal, required=True, help="quantity token to withdraw")
parser.add_argument("--account-address", type=PublicKey,
help="address of the specific account to use, if more than one available")
parser.add_argument("--allow-borrow", action="store_true", default=False,
help="allow borrowing to fund the withdrawal")
parser.add_argument(
"--symbol", type=str, required=True, help="token symbol to withdraw (e.g. USDC)"
)
parser.add_argument(
"--quantity", type=Decimal, required=True, help="quantity token to withdraw"
)
parser.add_argument(
"--account-address",
type=PublicKey,
help="address of the specific account to use, if more than one available",
)
parser.add_argument(
"--allow-borrow",
action="store_true",
default=False,
help="allow borrowing to fund the withdrawal",
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
group = mango.Group.load(context, context.group_address)
account = mango.Account.load_for_owner_by_address(context, wallet.address, group, args.account_address)
account = mango.Account.load_for_owner_by_address(
context, wallet.address, group, args.account_address
)
instrument = context.instrument_lookup.find_by_symbol(args.symbol)
if instrument is None:
raise Exception(f"Could not find instrument with symbol '{args.symbol}'.")
token: mango.Token = mango.Token.ensure(instrument)
token_account = mango.TokenAccount.fetch_or_create_largest_for_owner_and_token(context, wallet.keypair, token)
token_account = mango.TokenAccount.fetch_or_create_largest_for_owner_and_token(
context, wallet.keypair, token
)
withdrawal_value = mango.InstrumentValue(token, args.quantity)
withdrawal_token_account = mango.TokenAccount(
token_account.account_info, token_account.version, token_account.owner, withdrawal_value)
token_account.account_info,
token_account.version,
token_account.owner,
withdrawal_value,
)
token_bank = group.token_bank_by_instrument(token)
root_bank = token_bank.ensure_root_bank(context)
@ -45,7 +63,15 @@ node_bank = root_bank.pick_node_bank(context)
signers: mango.CombinableInstructions = mango.CombinableInstructions.from_wallet(wallet)
withdraw = mango.build_withdraw_instructions(
context, wallet, group, account, root_bank, node_bank, withdrawal_token_account, args.allow_borrow)
context,
wallet,
group,
account,
root_bank,
node_bank,
withdrawal_token_account,
args.allow_borrow,
)
all_instructions = signers + withdraw
transaction_ids = all_instructions.execute(context)

View File

@ -8,46 +8,69 @@ from decimal import Decimal
from solana.publickey import PublicKey
sys.path.insert(0, os.path.abspath(
os.path.join(os.path.dirname(__file__), "..")))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
import mango # nopep8
parser = argparse.ArgumentParser(
description="Wraps Pure SOL to Wrapped SOL and adds it to the first Wrapped SOL account, creating that account if it doesn't exist.")
description="Wraps Pure SOL to Wrapped SOL and adds it to the first Wrapped SOL account, creating that account if it doesn't exist."
)
mango.ContextBuilder.add_command_line_parameters(parser)
mango.Wallet.add_command_line_parameters(parser)
parser.add_argument("--quantity", type=Decimal, required=True, help="quantity of SOL to wrap")
parser.add_argument(
"--quantity", type=Decimal, required=True, help="quantity of SOL to wrap"
)
args: argparse.Namespace = mango.parse_args(parser)
context = mango.ContextBuilder.from_command_line_parameters(args)
wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
wrapped_sol: mango.Token = mango.Token.ensure(context.instrument_lookup.find_by_symbol_or_raise("SOL"))
wrapped_sol: mango.Token = mango.Token.ensure(
context.instrument_lookup.find_by_symbol_or_raise("SOL")
)
amount_to_transfer = int(args.quantity * mango.SOL_DECIMAL_DIVISOR)
signers: mango.CombinableInstructions = mango.CombinableInstructions.from_signers([wallet.keypair])
signers: mango.CombinableInstructions = mango.CombinableInstructions.from_signers(
[wallet.keypair]
)
all_instructions = signers
token_accounts = mango.TokenAccount.fetch_all_for_owner_and_token(context, wallet.address, wrapped_sol)
token_accounts = mango.TokenAccount.fetch_all_for_owner_and_token(
context, wallet.address, wrapped_sol
)
mango.output("Wrapping SOL:")
if len(token_accounts) == 0:
create_instructions = mango.build_create_associated_spl_account_instructions(context, wallet, wrapped_sol)
destination_wrapped_sol_address: PublicKey = create_instructions.instructions[0].keys[1].pubkey
create_instructions = mango.build_create_associated_spl_account_instructions(
context, wallet, wrapped_sol
)
destination_wrapped_sol_address: PublicKey = (
create_instructions.instructions[0].keys[1].pubkey
)
all_instructions += create_instructions
else:
destination_wrapped_sol_address = token_accounts[0].address
create_temporary_account_instructions = mango.build_create_spl_account_instructions(
context, wallet, wrapped_sol, amount_to_transfer)
temporary_wrapped_sol_address = create_temporary_account_instructions.signers[0].public_key
context, wallet, wrapped_sol, amount_to_transfer
)
temporary_wrapped_sol_address = create_temporary_account_instructions.signers[
0
].public_key
all_instructions += create_temporary_account_instructions
mango.output(f" Temporary account: {temporary_wrapped_sol_address}")
mango.output(f" Source: {wallet.address}")
mango.output(f" Destination: {destination_wrapped_sol_address}")
wrap_instruction = mango.build_transfer_spl_tokens_instructions(
context, wallet, wrapped_sol, temporary_wrapped_sol_address, destination_wrapped_sol_address, args.quantity)
close_instruction = mango.build_close_spl_account_instructions(context, wallet, temporary_wrapped_sol_address)
context,
wallet,
wrapped_sol,
temporary_wrapped_sol_address,
destination_wrapped_sol_address,
args.quantity,
)
close_instruction = mango.build_close_spl_account_instructions(
context, wallet, temporary_wrapped_sol_address
)
all_instructions = all_instructions + wrap_instruction + close_instruction
transaction_ids = all_instructions.execute(context)

View File

@ -11,9 +11,13 @@ from .account import Account as Account
from .account import AccountSlot as AccountSlot
from .accountflags import AccountFlags as AccountFlags
from .accountinfo import AccountInfo as AccountInfo
from .accountinfoconverter import build_account_info_converter as build_account_info_converter
from .accountinfoconverter import (
build_account_info_converter as build_account_info_converter,
)
from .accountinstrumentvalues import AccountInstrumentValues as AccountInstrumentValues
from .accountinstrumentvalues import PricedAccountInstrumentValues as PricedAccountInstrumentValues
from .accountinstrumentvalues import (
PricedAccountInstrumentValues as PricedAccountInstrumentValues,
)
from .accountliquidator import AccountLiquidator as AccountLiquidator
from .accountliquidator import NullAccountLiquidator as NullAccountLiquidator
from .accountscout import AccountScout as AccountScout
@ -39,9 +43,15 @@ from .client import RateLimitException as RateLimitException
from .client import RPCCaller as RPCCaller
from .client import SlotHolder as SlotHolder
from .client import StaleSlotException as StaleSlotException
from .client import TooManyRequestsRateLimitException as TooManyRequestsRateLimitException
from .client import TooMuchBandwidthRateLimitException as TooMuchBandwidthRateLimitException
from .client import TransactionAlreadyProcessedException as TransactionAlreadyProcessedException
from .client import (
TooManyRequestsRateLimitException as TooManyRequestsRateLimitException,
)
from .client import (
TooMuchBandwidthRateLimitException as TooMuchBandwidthRateLimitException,
)
from .client import (
TransactionAlreadyProcessedException as TransactionAlreadyProcessedException,
)
from .client import TransactionException as TransactionException
from .combinableinstructions import CombinableInstructions as CombinableInstructions
from .constants import MangoConstants as MangoConstants
@ -54,7 +64,9 @@ from .constants import WARNING_DISCLAIMER_TEXT as WARNING_DISCLAIMER_TEXT
from .constants import version as version
from .context import Context as Context
from .contextbuilder import ContextBuilder as ContextBuilder
from .createmarketoperations import create_market_instruction_builder as create_market_instruction_builder
from .createmarketoperations import (
create_market_instruction_builder as create_market_instruction_builder,
)
from .createmarketoperations import create_market_operations as create_market_operations
from .encoding import decode_binary as decode_binary
from .encoding import encode_binary as encode_binary
@ -73,34 +85,80 @@ from .idsjsonmarketlookup import IdsJsonMarketLookup as IdsJsonMarketLookup
from .inventory import Inventory as Inventory
from .inventory import PerpInventoryAccountWatcher as PerpInventoryAccountWatcher
from .inventory import SpotInventoryAccountWatcher as SpotInventoryAccountWatcher
from .instructions import build_cancel_all_perp_orders_instructions as build_cancel_all_perp_orders_instructions
from .instructions import build_cancel_perp_order_instructions as build_cancel_perp_order_instructions
from .instructions import build_cancel_spot_order_instructions as build_cancel_spot_order_instructions
from .instructions import build_close_spl_account_instructions as build_close_spl_account_instructions
from .instructions import build_create_account_instructions as build_create_account_instructions
from .instructions import build_create_associated_spl_account_instructions as build_create_associated_spl_account_instructions
from .instructions import build_create_solana_account_instructions as build_create_solana_account_instructions
from .instructions import build_create_spl_account_instructions as build_create_spl_account_instructions
from .instructions import build_create_serum_open_orders_instructions as build_create_serum_open_orders_instructions
from .instructions import (
build_cancel_all_perp_orders_instructions as build_cancel_all_perp_orders_instructions,
)
from .instructions import (
build_cancel_perp_order_instructions as build_cancel_perp_order_instructions,
)
from .instructions import (
build_cancel_spot_order_instructions as build_cancel_spot_order_instructions,
)
from .instructions import (
build_close_spl_account_instructions as build_close_spl_account_instructions,
)
from .instructions import (
build_create_account_instructions as build_create_account_instructions,
)
from .instructions import (
build_create_associated_spl_account_instructions as build_create_associated_spl_account_instructions,
)
from .instructions import (
build_create_solana_account_instructions as build_create_solana_account_instructions,
)
from .instructions import (
build_create_spl_account_instructions as build_create_spl_account_instructions,
)
from .instructions import (
build_create_serum_open_orders_instructions as build_create_serum_open_orders_instructions,
)
from .instructions import build_deposit_instructions as build_deposit_instructions
from .instructions import build_faucet_airdrop_instructions as build_faucet_airdrop_instructions
from .instructions import build_mango_consume_events_instructions as build_mango_consume_events_instructions
from .instructions import build_place_perp_order_instructions as build_place_perp_order_instructions
from .instructions import build_redeem_accrued_mango_instructions as build_redeem_accrued_mango_instructions
from .instructions import build_register_referrer_id_instructions as build_register_referrer_id_instructions
from .instructions import build_serum_consume_events_instructions as build_serum_consume_events_instructions
from .instructions import build_serum_place_order_instructions as build_serum_place_order_instructions
from .instructions import build_serum_settle_instructions as build_serum_settle_instructions
from .instructions import build_set_account_delegate_instructions as build_set_account_delegate_instructions
from .instructions import build_set_referrer_memory_instructions as build_set_referrer_memory_instructions
from .instructions import build_spot_place_order_instructions as build_spot_place_order_instructions
from .instructions import build_transfer_spl_tokens_instructions as build_transfer_spl_tokens_instructions
from .instructions import build_unset_account_delegate_instructions as build_unset_account_delegate_instructions
from .instructions import (
build_faucet_airdrop_instructions as build_faucet_airdrop_instructions,
)
from .instructions import (
build_mango_consume_events_instructions as build_mango_consume_events_instructions,
)
from .instructions import (
build_place_perp_order_instructions as build_place_perp_order_instructions,
)
from .instructions import (
build_redeem_accrued_mango_instructions as build_redeem_accrued_mango_instructions,
)
from .instructions import (
build_register_referrer_id_instructions as build_register_referrer_id_instructions,
)
from .instructions import (
build_serum_consume_events_instructions as build_serum_consume_events_instructions,
)
from .instructions import (
build_serum_place_order_instructions as build_serum_place_order_instructions,
)
from .instructions import (
build_serum_settle_instructions as build_serum_settle_instructions,
)
from .instructions import (
build_set_account_delegate_instructions as build_set_account_delegate_instructions,
)
from .instructions import (
build_set_referrer_memory_instructions as build_set_referrer_memory_instructions,
)
from .instructions import (
build_spot_place_order_instructions as build_spot_place_order_instructions,
)
from .instructions import (
build_transfer_spl_tokens_instructions as build_transfer_spl_tokens_instructions,
)
from .instructions import (
build_unset_account_delegate_instructions as build_unset_account_delegate_instructions,
)
from .instructions import build_withdraw_instructions as build_withdraw_instructions
from .instructionreporter import InstructionReporter as InstructionReporter
from .instructionreporter import SerumInstructionReporter as SerumInstructionReporter
from .instructionreporter import MangoInstructionReporter as MangoInstructionReporter
from .instructionreporter import CompoundInstructionReporter as CompoundInstructionReporter
from .instructionreporter import (
CompoundInstructionReporter as CompoundInstructionReporter,
)
from .instructiontype import InstructionType as InstructionType
from .instrumentlookup import InstrumentLookup as InstrumentLookup
from .instrumentlookup import NullInstrumentLookup as NullInstrumentLookup
@ -127,7 +185,9 @@ from .marketlookup import MarketLookup as MarketLookup
from .marketlookup import NullMarketLookup as NullMarketLookup
from .marketoperations import MarketInstructionBuilder as MarketInstructionBuilder
from .marketoperations import MarketOperations as MarketOperations
from .marketoperations import NullMarketInstructionBuilder as NullMarketInstructionBuilder
from .marketoperations import (
NullMarketInstructionBuilder as NullMarketInstructionBuilder,
)
from .marketoperations import NullMarketOperations as NullMarketOperations
from .metadata import Metadata as Metadata
from .modelstate import EventQueue as EventQueue
@ -152,11 +212,17 @@ from .observables import FunctionObserver as FunctionObserver
from .observables import LatestItemObserverSubscriber as LatestItemObserverSubscriber
from .observables import NullObserverSubscriber as NullObserverSubscriber
from .observables import PrintingObserverSubscriber as PrintingObserverSubscriber
from .observables import TimestampedPrintingObserverSubscriber as TimestampedPrintingObserverSubscriber
from .observables import create_backpressure_skipping_observer as create_backpressure_skipping_observer
from .observables import (
TimestampedPrintingObserverSubscriber as TimestampedPrintingObserverSubscriber,
)
from .observables import (
create_backpressure_skipping_observer as create_backpressure_skipping_observer,
)
from .observables import debug_print_item as debug_print_item
from .observables import log_subscription_error as log_subscription_error
from .observables import observable_pipeline_error_reporter as observable_pipeline_error_reporter
from .observables import (
observable_pipeline_error_reporter as observable_pipeline_error_reporter,
)
from .openorders import OpenOrders as OpenOrders
from .oracle import Oracle as Oracle
from .oracle import OracleProvider as OracleProvider
@ -172,19 +238,27 @@ from .orders import Side as Side
from .ownedinstrumentvalue import OwnedInstrumentValue as OwnedInstrumentValue
from .oraclefactory import create_oracle_provider as create_oracle_provider
from .output import output as output
from .parse_account_info_to_orders import parse_account_info_to_orders as parse_account_info_to_orders
from .parse_account_info_to_orders import (
parse_account_info_to_orders as parse_account_info_to_orders,
)
from .perpaccount import PerpAccount as PerpAccount
from .perpeventqueue import PerpEvent as PerpEvent
from .perpeventqueue import PerpEventQueue as PerpEventQueue
from .perpeventqueue import PerpFillEvent as PerpFillEvent
from .perpeventqueue import PerpOutEvent as PerpOutEvent
from .perpeventqueue import PerpUnknownEvent as PerpUnknownEvent
from .perpeventqueue import UnseenAccountFillEventTracker as UnseenAccountFillEventTracker
from .perpeventqueue import UnseenPerpEventChangesTracker as UnseenPerpEventChangesTracker
from .perpeventqueue import (
UnseenAccountFillEventTracker as UnseenAccountFillEventTracker,
)
from .perpeventqueue import (
UnseenPerpEventChangesTracker as UnseenPerpEventChangesTracker,
)
from .perpmarket import PerpMarket as PerpMarket
from .perpmarket import PerpMarketStub as PerpMarketStub
from .perpmarketdetails import PerpMarketDetails as PerpMarketDetails
from .perpmarketoperations import PerpMarketInstructionBuilder as PerpMarketInstructionBuilder
from .perpmarketoperations import (
PerpMarketInstructionBuilder as PerpMarketInstructionBuilder,
)
from .perpmarketoperations import PerpMarketOperations as PerpMarketOperations
from .perpopenorders import PerpOpenOrders as PerpOpenOrders
from .placedorder import PlacedOrder as PlacedOrder
@ -194,15 +268,21 @@ from .reconnectingwebsocket import ReconnectingWebsocket as ReconnectingWebsocke
from .retrier import RetryWithPauses as RetryWithPauses
from .retrier import retry_context as retry_context
from .serumeventqueue import SerumEventQueue as SerumEventQueue
from .serumeventqueue import UnseenSerumEventChangesTracker as UnseenSerumEventChangesTracker
from .serumeventqueue import (
UnseenSerumEventChangesTracker as UnseenSerumEventChangesTracker,
)
from .serummarket import SerumMarket as SerumMarket
from .serummarket import SerumMarketStub as SerumMarketStub
from .serummarketlookup import SerumMarketLookup as SerumMarketLookup
from .serummarketoperations import SerumMarketInstructionBuilder as SerumMarketInstructionBuilder
from .serummarketoperations import (
SerumMarketInstructionBuilder as SerumMarketInstructionBuilder,
)
from .serummarketoperations import SerumMarketOperations as SerumMarketOperations
from .spotmarket import SpotMarket as SpotMarket
from .spotmarket import SpotMarketStub as SpotMarketStub
from .spotmarketoperations import SpotMarketInstructionBuilder as SpotMarketInstructionBuilder
from .spotmarketoperations import (
SpotMarketInstructionBuilder as SpotMarketInstructionBuilder,
)
from .spotmarketoperations import SpotMarketOperations as SpotMarketOperations
from .text import indent_collection_as_str as indent_collection_as_str
from .text import indent_item_by as indent_item_by
@ -220,8 +300,12 @@ from .tradeexecutor import NullTradeExecutor as NullTradeExecutor
from .tradeexecutor import TradeExecutor as TradeExecutor
from .tradehistory import TradeHistory as TradeHistory
from .transactionscout import TransactionScout as TransactionScout
from .transactionscout import fetch_all_recent_transaction_signatures as fetch_all_recent_transaction_signatures
from .transactionscout import mango_instruction_from_response as mango_instruction_from_response
from .transactionscout import (
fetch_all_recent_transaction_signatures as fetch_all_recent_transaction_signatures,
)
from .transactionscout import (
mango_instruction_from_response as mango_instruction_from_response,
)
from .valuation import AccountValuation as AccountValuation
from .valuation import TokenValuation as TokenValuation
from .valuation import Valuation as Valuation
@ -235,7 +319,9 @@ from .walletbalancer import NullWalletBalancer as NullWalletBalancer
from .walletbalancer import PercentageTargetBalance as PercentageTargetBalance
from .walletbalancer import TargetBalance as TargetBalance
from .walletbalancer import WalletBalancer as WalletBalancer
from .walletbalancer import calculate_required_balance_changes as calculate_required_balance_changes
from .walletbalancer import (
calculate_required_balance_changes as calculate_required_balance_changes,
)
from .walletbalancer import parse_fixed_target_balance as parse_fixed_target_balance
from .walletbalancer import parse_target_balance as parse_target_balance
from .walletbalancer import sort_changes_for_trades as sort_changes_for_trades
@ -254,13 +340,23 @@ from .watchers import build_orderbook_watcher as build_orderbook_watcher
from .watchers import build_serum_event_queue_watcher as build_serum_event_queue_watcher
from .watchers import build_spot_event_queue_watcher as build_spot_event_queue_watcher
from .watchers import build_perp_event_queue_watcher as build_perp_event_queue_watcher
from .websocketsubscription import IndividualWebSocketSubscriptionManager as IndividualWebSocketSubscriptionManager
from .websocketsubscription import SharedWebSocketSubscriptionManager as SharedWebSocketSubscriptionManager
from .websocketsubscription import WebSocketAccountSubscription as WebSocketAccountSubscription
from .websocketsubscription import (
IndividualWebSocketSubscriptionManager as IndividualWebSocketSubscriptionManager,
)
from .websocketsubscription import (
SharedWebSocketSubscriptionManager as SharedWebSocketSubscriptionManager,
)
from .websocketsubscription import (
WebSocketAccountSubscription as WebSocketAccountSubscription,
)
from .websocketsubscription import WebSocketLogSubscription as WebSocketLogSubscription
from .websocketsubscription import WebSocketProgramSubscription as WebSocketProgramSubscription
from .websocketsubscription import (
WebSocketProgramSubscription as WebSocketProgramSubscription,
)
from .websocketsubscription import WebSocketSubscription as WebSocketSubscription
from .websocketsubscription import WebSocketSubscriptionManager as WebSocketSubscriptionManager
from .websocketsubscription import (
WebSocketSubscriptionManager as WebSocketSubscriptionManager,
)
from .layouts import layouts

View File

@ -44,7 +44,19 @@ from .version import Version
# `AccountSlot` gathers slot items together instead of separate arrays.
#
class AccountSlot:
def __init__(self, index: int, base_instrument: Instrument, base_token_bank: typing.Optional[TokenBank], quote_token_bank: TokenBank, raw_deposit: Decimal, deposit: InstrumentValue, raw_borrow: Decimal, borrow: InstrumentValue, spot_open_orders: typing.Optional[PublicKey], perp_account: typing.Optional[PerpAccount]) -> None:
def __init__(
self,
index: int,
base_instrument: Instrument,
base_token_bank: typing.Optional[TokenBank],
quote_token_bank: TokenBank,
raw_deposit: Decimal,
deposit: InstrumentValue,
raw_borrow: Decimal,
borrow: InstrumentValue,
spot_open_orders: typing.Optional[PublicKey],
perp_account: typing.Optional[PerpAccount],
) -> None:
self.index: int = index
self.base_instrument: Instrument = base_instrument
self.base_token_bank: typing.Optional[TokenBank] = base_token_bank
@ -94,14 +106,26 @@ class Account(AddressableAccount):
def __sum_pos(dataframe: pandas.DataFrame, name: str) -> Decimal:
return typing.cast(Decimal, dataframe.loc[dataframe[name] > 0, name].sum())
def __init__(self, account_info: AccountInfo, version: Version,
meta_data: Metadata, group_name: str, group_address: PublicKey, owner: PublicKey,
info: str, shared_quote: AccountSlot,
in_margin_basket: typing.Sequence[bool],
slot_indices: typing.Sequence[bool],
base_slots: typing.Sequence[AccountSlot],
msrm_amount: Decimal, being_liquidated: bool, is_bankrupt: bool,
advanced_orders: PublicKey, not_upgradable: bool, delegate: PublicKey) -> None:
def __init__(
self,
account_info: AccountInfo,
version: Version,
meta_data: Metadata,
group_name: str,
group_address: PublicKey,
owner: PublicKey,
info: str,
shared_quote: AccountSlot,
in_margin_basket: typing.Sequence[bool],
slot_indices: typing.Sequence[bool],
base_slots: typing.Sequence[AccountSlot],
msrm_amount: Decimal,
being_liquidated: bool,
is_bankrupt: bool,
advanced_orders: PublicKey,
not_upgradable: bool,
delegate: PublicKey,
) -> None:
super().__init__(account_info)
self.version: Version = version
@ -152,7 +176,9 @@ class Account(AddressableAccount):
@property
def deposits_by_index(self) -> typing.Sequence[typing.Optional[InstrumentValue]]:
return [slot.deposit if slot is not None else None for slot in self.slots_by_index]
return [
slot.deposit if slot is not None else None for slot in self.slots_by_index
]
@property
def borrows(self) -> typing.Sequence[InstrumentValue]:
@ -160,7 +186,9 @@ class Account(AddressableAccount):
@property
def borrows_by_index(self) -> typing.Sequence[typing.Optional[InstrumentValue]]:
return [slot.borrow if slot is not None else None for slot in self.slots_by_index]
return [
slot.borrow if slot is not None else None for slot in self.slots_by_index
]
@property
def net_values(self) -> typing.Sequence[InstrumentValue]:
@ -168,35 +196,60 @@ class Account(AddressableAccount):
@property
def net_values_by_index(self) -> typing.Sequence[typing.Optional[InstrumentValue]]:
return [slot.net_value if slot is not None else None for slot in self.slots_by_index]
return [
slot.net_value if slot is not None else None for slot in self.slots_by_index
]
@property
def spot_open_orders(self) -> typing.Sequence[PublicKey]:
return [slot.spot_open_orders for slot in self.base_slots if slot.spot_open_orders is not None]
return [
slot.spot_open_orders
for slot in self.base_slots
if slot.spot_open_orders is not None
]
@property
def spot_open_orders_by_index(self) -> typing.Sequence[typing.Optional[PublicKey]]:
return [slot.spot_open_orders if slot is not None else None for slot in self.slots_by_index]
return [
slot.spot_open_orders if slot is not None else None
for slot in self.slots_by_index
]
@property
def perp_accounts(self) -> typing.Sequence[PerpAccount]:
return [slot.perp_account for slot in self.base_slots if slot.perp_account is not None]
return [
slot.perp_account
for slot in self.base_slots
if slot.perp_account is not None
]
@property
def perp_accounts_by_index(self) -> typing.Sequence[typing.Optional[PerpAccount]]:
return [slot.perp_account if slot is not None else None for slot in self.slots_by_index]
return [
slot.perp_account if slot is not None else None
for slot in self.slots_by_index
]
@staticmethod
def from_layout(layout: typing.Any, account_info: AccountInfo, version: Version, group: Group, cache: Cache) -> "Account":
def from_layout(
layout: typing.Any,
account_info: AccountInfo,
version: Version,
group: Group,
cache: Cache,
) -> "Account":
meta_data = Metadata.from_layout(layout.meta_data)
owner: PublicKey = layout.owner
info: str = layout.info
mngo_token = group.liquidity_incentive_token
in_margin_basket: typing.Sequence[bool] = list([bool(in_basket) for in_basket in layout.in_margin_basket])
in_margin_basket: typing.Sequence[bool] = list(
[bool(in_basket) for in_basket in layout.in_margin_basket]
)
active_in_basket: typing.List[bool] = []
slots: typing.List[AccountSlot] = []
placed_orders_all_markets: typing.List[typing.List[PlacedOrder]] = [[]
for _ in range(len(group.slot_indices) - 1)]
placed_orders_all_markets: typing.List[typing.List[PlacedOrder]] = [
[] for _ in range(len(group.slot_indices) - 1)
]
for index, order_market in enumerate(layout.order_market):
if order_market != 0xFF:
side = Side.from_value(layout.order_side[index])
@ -219,16 +272,23 @@ class Account(AddressableAccount):
intrinsic_borrow: Decimal = Decimal(0)
if token_bank is not None:
raw_deposit = layout.deposits[index]
root_bank_cache: typing.Optional[RootBankCache] = token_bank.root_bank_cache_from_cache(
cache, index)
root_bank_cache: typing.Optional[
RootBankCache
] = token_bank.root_bank_cache_from_cache(cache, index)
if root_bank_cache is None:
raise Exception(f"No root bank cache found for token {token_bank} at index {index}")
raise Exception(
f"No root bank cache found for token {token_bank} at index {index}"
)
intrinsic_deposit = root_bank_cache.deposit_index * raw_deposit
raw_borrow = layout.borrows[index]
intrinsic_borrow = root_bank_cache.borrow_index * raw_borrow
deposit = InstrumentValue(instrument, instrument.shift_to_decimals(intrinsic_deposit))
borrow = InstrumentValue(instrument, instrument.shift_to_decimals(intrinsic_borrow))
deposit = InstrumentValue(
instrument, instrument.shift_to_decimals(intrinsic_deposit)
)
borrow = InstrumentValue(
instrument, instrument.shift_to_decimals(intrinsic_borrow)
)
perp_open_orders = PerpOpenOrders(placed_orders_all_markets[index])
@ -238,11 +298,21 @@ class Account(AddressableAccount):
quote_token,
perp_open_orders,
group_slot.perp_lot_size_converter,
mngo_token)
mngo_token,
)
spot_open_orders = layout.spot_open_orders[index]
account_slot: AccountSlot = AccountSlot(index, instrument, token_bank, quote_token_bank,
raw_deposit, deposit, raw_borrow, borrow,
spot_open_orders, perp_account)
account_slot: AccountSlot = AccountSlot(
index,
instrument,
token_bank,
quote_token_bank,
raw_deposit,
deposit,
raw_borrow,
borrow,
spot_open_orders,
perp_account,
)
slots += [account_slot]
active_in_basket += [True]
@ -251,18 +321,36 @@ class Account(AddressableAccount):
quote_index: int = len(layout.deposits) - 1
raw_quote_deposit: Decimal = layout.deposits[quote_index]
quote_root_bank_cache: typing.Optional[RootBankCache] = quote_token_bank.root_bank_cache_from_cache(
cache, quote_index)
quote_root_bank_cache: typing.Optional[
RootBankCache
] = quote_token_bank.root_bank_cache_from_cache(cache, quote_index)
if quote_root_bank_cache is None:
raise Exception(f"No root bank cache found for quote token {quote_token_bank} at index {index}")
intrinsic_quote_deposit = quote_root_bank_cache.deposit_index * raw_quote_deposit
quote_deposit = InstrumentValue(quote_token, quote_token.shift_to_decimals(intrinsic_quote_deposit))
raise Exception(
f"No root bank cache found for quote token {quote_token_bank} at index {index}"
)
intrinsic_quote_deposit = (
quote_root_bank_cache.deposit_index * raw_quote_deposit
)
quote_deposit = InstrumentValue(
quote_token, quote_token.shift_to_decimals(intrinsic_quote_deposit)
)
raw_quote_borrow: Decimal = layout.borrows[quote_index]
intrinsic_quote_borrow = quote_root_bank_cache.borrow_index * raw_quote_borrow
quote_borrow = InstrumentValue(quote_token, quote_token.shift_to_decimals(intrinsic_quote_borrow))
quote: AccountSlot = AccountSlot(len(layout.deposits) - 1, quote_token_bank.token, quote_token_bank,
quote_token_bank, raw_quote_deposit, quote_deposit, raw_quote_borrow,
quote_borrow, None, None)
quote_borrow = InstrumentValue(
quote_token, quote_token.shift_to_decimals(intrinsic_quote_borrow)
)
quote: AccountSlot = AccountSlot(
len(layout.deposits) - 1,
quote_token_bank.token,
quote_token_bank,
quote_token_bank,
raw_quote_deposit,
quote_deposit,
raw_quote_borrow,
quote_borrow,
None,
None,
)
msrm_amount: Decimal = layout.msrm_amount
being_liquidated: bool = bool(layout.being_liquidated)
@ -271,16 +359,33 @@ class Account(AddressableAccount):
not_upgradable: bool = bool(layout.not_upgradable)
delegate: PublicKey = layout.delegate
return Account(account_info, version, meta_data, group.name, group.address, owner, info, quote,
in_margin_basket, active_in_basket, slots, msrm_amount, being_liquidated, is_bankrupt,
advanced_orders, not_upgradable, delegate)
return Account(
account_info,
version,
meta_data,
group.name,
group.address,
owner,
info,
quote,
in_margin_basket,
active_in_basket,
slots,
msrm_amount,
being_liquidated,
is_bankrupt,
advanced_orders,
not_upgradable,
delegate,
)
@staticmethod
def parse(account_info: AccountInfo, group: Group, cache: Cache) -> "Account":
data = account_info.data
if len(data) != layouts.MANGO_ACCOUNT.sizeof():
raise Exception(
f"Account 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)
return Account.from_layout(layout, account_info, Version.V3, group, cache)
@ -298,92 +403,105 @@ class Account(AddressableAccount):
# mango_group is just after the METADATA, which is the first entry.
group_offset = layouts.METADATA.sizeof()
# owner is just after mango_group in the layout, and it's a PublicKey which is 32 bytes.
filters = [
MemcmpOpts(
offset=group_offset,
bytes=encode_key(group.address)
)
]
filters = [MemcmpOpts(offset=group_offset, bytes=encode_key(group.address))]
results = context.client.get_program_accounts(
context.mango_program_address, memcmp_opts=filters, data_size=layouts.MANGO_ACCOUNT.sizeof())
context.mango_program_address,
memcmp_opts=filters,
data_size=layouts.MANGO_ACCOUNT.sizeof(),
)
cache: Cache = group.fetch_cache(context)
accounts: typing.List[Account] = []
for account_data in results:
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 = Account.parse(account_info, group, cache)
accounts += [account]
return accounts
@staticmethod
def load_all_for_owner(context: Context, owner: PublicKey, group: Group) -> typing.Sequence["Account"]:
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.
group_offset = layouts.METADATA.sizeof()
# owner is just after mango_group in the layout, and it's a PublicKey which is 32 bytes.
owner_offset = group_offset + 32
filters = [
MemcmpOpts(
offset=group_offset,
bytes=encode_key(group.address)
),
MemcmpOpts(
offset=owner_offset,
bytes=encode_key(owner)
)
MemcmpOpts(offset=group_offset, bytes=encode_key(group.address)),
MemcmpOpts(offset=owner_offset, bytes=encode_key(owner)),
]
results = context.client.get_program_accounts(
context.mango_program_address, memcmp_opts=filters, data_size=layouts.MANGO_ACCOUNT.sizeof())
context.mango_program_address,
memcmp_opts=filters,
data_size=layouts.MANGO_ACCOUNT.sizeof(),
)
cache: Cache = group.fetch_cache(context)
accounts: typing.List[Account] = []
for account_data in results:
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 = Account.parse(account_info, group, cache)
accounts += [account]
return accounts
@staticmethod
def load_all_for_delegate(context: Context, delegate: PublicKey, group: Group) -> typing.Sequence["Account"]:
def load_all_for_delegate(
context: Context, delegate: PublicKey, group: Group
) -> typing.Sequence["Account"]:
# mango_group is just after the METADATA, which is the first entry.
group_offset = layouts.METADATA.sizeof()
# delegate is a PublicKey which is 32 bytes that ends 5 bytes before the end of the layout
delegate_offset = layouts.MANGO_ACCOUNT.sizeof() - 37
filters = [
MemcmpOpts(
offset=group_offset,
bytes=encode_key(group.address)
),
MemcmpOpts(
offset=delegate_offset,
bytes=encode_key(delegate)
)
MemcmpOpts(offset=group_offset, bytes=encode_key(group.address)),
MemcmpOpts(offset=delegate_offset, bytes=encode_key(delegate)),
]
results = context.client.get_program_accounts(
context.mango_program_address, memcmp_opts=filters, data_size=layouts.MANGO_ACCOUNT.sizeof())
context.mango_program_address,
memcmp_opts=filters,
data_size=layouts.MANGO_ACCOUNT.sizeof(),
)
cache: Cache = group.fetch_cache(context)
accounts: typing.List[Account] = []
for account_data in results:
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 = Account.parse(account_info, group, cache)
accounts += [account]
return accounts
@staticmethod
def load_for_owner_by_address(context: Context, owner: PublicKey, group: Group, account_address: typing.Optional[PublicKey]) -> "Account":
def load_for_owner_by_address(
context: Context,
owner: PublicKey,
group: Group,
account_address: typing.Optional[PublicKey],
) -> "Account":
if account_address is not None:
return Account.load(context, account_address, group)
accounts: typing.Sequence[Account] = Account.load_all_for_owner(context, owner, group)
accounts: typing.Sequence[Account] = Account.load_all_for_owner(
context, owner, group
)
if len(accounts) > 1:
raise Exception(f"More than 1 Mango account for owner '{owner}' and which to choose not specified.")
raise Exception(
f"More than 1 Mango account for owner '{owner}' and which to choose not specified."
)
return accounts[0]
def slot_by_instrument_or_none(self, instrument: Instrument) -> typing.Optional[AccountSlot]:
def slot_by_instrument_or_none(
self, instrument: Instrument
) -> typing.Optional[AccountSlot]:
for slot in self.slots:
if slot.base_instrument == instrument:
return slot
@ -397,7 +515,9 @@ class Account(AddressableAccount):
raise Exception(f"Could not find token {instrument} in account {self.address}")
def slot_by_spot_open_orders_or_none(self, spot_open_orders: PublicKey) -> typing.Optional[AccountSlot]:
def slot_by_spot_open_orders_or_none(
self, spot_open_orders: PublicKey
) -> typing.Optional[AccountSlot]:
for slot in self.slots:
if slot.spot_open_orders == spot_open_orders:
return slot
@ -405,48 +525,73 @@ class Account(AddressableAccount):
return None
def slot_by_spot_open_orders(self, spot_open_orders: PublicKey) -> AccountSlot:
slot: typing.Optional[AccountSlot] = self.slot_by_spot_open_orders_or_none(spot_open_orders)
slot: typing.Optional[AccountSlot] = self.slot_by_spot_open_orders_or_none(
spot_open_orders
)
if slot is not None:
return slot
raise Exception(f"Could not find spot open orders {spot_open_orders} in account {self.address}")
raise Exception(
f"Could not find spot open orders {spot_open_orders} in account {self.address}"
)
def load_all_spot_open_orders(self, context: Context) -> typing.Dict[str, OpenOrders]:
spot_open_orders_account_infos = AccountInfo.load_multiple(context, self.spot_open_orders)
def load_all_spot_open_orders(
self, context: Context
) -> typing.Dict[str, OpenOrders]:
spot_open_orders_account_infos = AccountInfo.load_multiple(
context, self.spot_open_orders
)
spot_open_orders_account_infos_by_address = {
str(account_info.address): account_info for account_info in spot_open_orders_account_infos}
str(account_info.address): account_info
for account_info in spot_open_orders_account_infos
}
spot_open_orders: typing.Dict[str, OpenOrders] = {}
for slot in self.base_slots:
if slot.spot_open_orders is not None:
account_info = spot_open_orders_account_infos_by_address[str(slot.spot_open_orders)]
oo = OpenOrders.parse(account_info, slot.base_instrument.decimals,
self.shared_quote.base_instrument.decimals)
account_info = spot_open_orders_account_infos_by_address[
str(slot.spot_open_orders)
]
oo = OpenOrders.parse(
account_info,
slot.base_instrument.decimals,
self.shared_quote.base_instrument.decimals,
)
spot_open_orders[str(slot.spot_open_orders)] = oo
return spot_open_orders
def update_spot_open_orders_for_market(self, spot_market_index: int, spot_open_orders: PublicKey) -> None:
def update_spot_open_orders_for_market(
self, spot_market_index: int, spot_open_orders: PublicKey
) -> None:
item_to_update = self.slots_by_index[spot_market_index]
if item_to_update is None:
raise Exception(f"Could not find AccountBasketItem in Account {self.address} at index {spot_market_index}.")
raise Exception(
f"Could not find AccountBasketItem in Account {self.address} at index {spot_market_index}."
)
item_to_update.spot_open_orders = spot_open_orders
def derive_referrer_memory_address(self, context: Context) -> PublicKey:
referrer_memory_address_and_nonce: typing.Tuple[PublicKey, int] = PublicKey.find_program_address(
[
bytes(self.address),
b"ReferrerMemory"
],
context.mango_program_address
referrer_memory_address_and_nonce: typing.Tuple[
PublicKey, int
] = PublicKey.find_program_address(
[bytes(self.address), b"ReferrerMemory"], context.mango_program_address
)
return referrer_memory_address_and_nonce[0]
def to_dataframe(self, group: Group, all_spot_open_orders: typing.Dict[str, OpenOrders], cache: Cache) -> pandas.DataFrame:
def to_dataframe(
self,
group: Group,
all_spot_open_orders: typing.Dict[str, OpenOrders],
cache: Cache,
) -> pandas.DataFrame:
asset_data = []
for slot in self.slots:
market_cache: typing.Optional[MarketCache] = group.market_cache_from_cache_or_none(
cache, slot.base_instrument)
price: InstrumentValue = group.token_price_from_cache(cache, slot.base_instrument)
market_cache: typing.Optional[
MarketCache
] = group.market_cache_from_cache_or_none(cache, slot.base_instrument)
price: InstrumentValue = group.token_price_from_cache(
cache, slot.base_instrument
)
spot_open_orders: typing.Optional[OpenOrders] = None
spot_health_base: Decimal = Decimal(0)
@ -456,7 +601,9 @@ class Account(AddressableAccount):
if slot.spot_open_orders is not None:
spot_open_orders = all_spot_open_orders[str(slot.spot_open_orders)]
if spot_open_orders is None:
raise Exception(f"OpenOrders address {slot.spot_open_orders} at index {slot.index} not loaded.")
raise Exception(
f"OpenOrders address {slot.spot_open_orders} at index {slot.index} not loaded."
)
# Here's a comment from ckamm in https://github.com/blockworks-foundation/mango-v3/pull/78/files
# that describes some of the health calculations.
@ -488,19 +635,25 @@ class Account(AddressableAccount):
# // That means scenario 1 leads to less health whenever |a + b| > |a|.
# base total if all bids were executed
spot_bids_base_net = slot.net_value.value + \
(spot_open_orders.quote_token_locked / price.value) + spot_open_orders.base_token_total
spot_bids_base_net = (
slot.net_value.value
+ (spot_open_orders.quote_token_locked / price.value)
+ spot_open_orders.base_token_total
)
# base total if all asks were executed
spot_asks_base_net = slot.net_value.value + spot_open_orders.base_token_free
spot_asks_base_net = (
slot.net_value.value + spot_open_orders.base_token_free
)
if abs(spot_bids_base_net) > abs(spot_asks_base_net):
spot_health_base = spot_bids_base_net
spot_health_quote = spot_open_orders.quote_token_free
else:
spot_health_base = spot_asks_base_net
spot_health_quote = (spot_open_orders.base_token_locked * price.value) + \
spot_open_orders.quote_token_total
spot_health_quote = (
spot_open_orders.base_token_locked * price.value
) + spot_open_orders.quote_token_total
# From Daffy in Discord 2021-11-23: https://discord.com/channels/791995070613159966/857699200279773204/912705017767677982
# --
@ -533,43 +686,83 @@ class Account(AddressableAccount):
perp_asset: Decimal = Decimal(0)
perp_liability: Decimal = Decimal(0)
perp_current_value: Decimal = Decimal(0)
if slot.perp_account is not None and not slot.perp_account.empty and market_cache is not None:
perp_market: typing.Optional[GroupSlotPerpMarket] = group.perp_markets_by_index[slot.index]
if (
slot.perp_account is not None
and not slot.perp_account.empty
and market_cache is not None
):
perp_market: typing.Optional[
GroupSlotPerpMarket
] = group.perp_markets_by_index[slot.index]
if perp_market is None:
raise Exception(f"Could not find perp market in Group at index {slot.index}.")
raise Exception(
f"Could not find perp market in Group at index {slot.index}."
)
perp_position = slot.perp_account.lot_size_converter.base_size_lots_to_number(
slot.perp_account.base_position)
perp_position = (
slot.perp_account.lot_size_converter.base_size_lots_to_number(
slot.perp_account.base_position
)
)
perp_notional_position = perp_position * price.value
perp_value = slot.perp_account.quote_position_raw
cached_perp_market: typing.Optional[PerpMarketCache] = market_cache.perp_market
cached_perp_market: typing.Optional[
PerpMarketCache
] = market_cache.perp_market
if cached_perp_market is None:
raise Exception(f"Could not find perp market in Cache at index {slot.index}.")
raise Exception(
f"Could not find perp market in Cache at index {slot.index}."
)
unsettled_funding = slot.perp_account.unsettled_funding(cached_perp_market)
bids_quantity = slot.perp_account.lot_size_converter.base_size_lots_to_number(
slot.perp_account.bids_quantity)
asks_quantity = slot.perp_account.lot_size_converter.base_size_lots_to_number(
slot.perp_account.asks_quantity)
taker_quote = slot.perp_account.lot_size_converter.quote_size_lots_to_number(
slot.perp_account.taker_quote)
unsettled_funding = slot.perp_account.unsettled_funding(
cached_perp_market
)
bids_quantity = (
slot.perp_account.lot_size_converter.base_size_lots_to_number(
slot.perp_account.bids_quantity
)
)
asks_quantity = (
slot.perp_account.lot_size_converter.base_size_lots_to_number(
slot.perp_account.asks_quantity
)
)
taker_quote = (
slot.perp_account.lot_size_converter.quote_size_lots_to_number(
slot.perp_account.taker_quote
)
)
perp_bids_base_net: Decimal = perp_position + bids_quantity
perp_asks_base_net: Decimal = perp_position - asks_quantity
perp_asset = slot.perp_account.asset_value(cached_perp_market, price.value)
perp_liability = slot.perp_account.liability_value(cached_perp_market, price.value)
perp_current_value = slot.perp_account.current_value(cached_perp_market, price.value)
perp_asset = slot.perp_account.asset_value(
cached_perp_market, price.value
)
perp_liability = slot.perp_account.liability_value(
cached_perp_market, price.value
)
perp_current_value = slot.perp_account.current_value(
cached_perp_market, price.value
)
quote_pos = slot.perp_account.quote_position / (10 ** self.shared_quote_token.decimals)
quote_pos = slot.perp_account.quote_position / (
10**self.shared_quote_token.decimals
)
if abs(perp_bids_base_net) > abs(perp_asks_base_net):
perp_health_base = perp_bids_base_net
perp_health_quote = (quote_pos + unsettled_funding) + \
taker_quote - (bids_quantity * price.value)
perp_health_quote = (
(quote_pos + unsettled_funding)
+ taker_quote
- (bids_quantity * price.value)
)
else:
perp_health_base = perp_asks_base_net
perp_health_quote = (quote_pos + unsettled_funding) + \
taker_quote + (asks_quantity * price.value)
perp_health_quote = (
(quote_pos + unsettled_funding)
+ taker_quote
+ (asks_quantity * price.value)
)
perp_health_base_value = perp_health_base * price.value
group_slot: typing.Optional[GroupSlot] = None
@ -615,10 +808,14 @@ class Account(AddressableAccount):
base_open_unsettled = spot_open_orders.base_token_free
base_open_locked = spot_open_orders.base_token_locked
base_open_total = spot_open_orders.base_token_total
quote_open_unsettled = (spot_open_orders.quote_token_free
+ spot_open_orders.referrer_rebate_accrued)
quote_open_unsettled = (
spot_open_orders.quote_token_free
+ spot_open_orders.referrer_rebate_accrued
)
quote_open_locked = spot_open_orders.quote_token_locked
base_total: Decimal = slot.deposit.value - slot.borrow.value + base_open_total
base_total: Decimal = (
slot.deposit.value - slot.borrow.value + base_open_total
)
base_total_value: Decimal = base_total * price.value
spot_init_value: Decimal
spot_maint_value: Decimal
@ -633,13 +830,21 @@ class Account(AddressableAccount):
if perp_health_base >= 0:
perp_init_value = perp_notional_position * perp_init_asset_weight
perp_maint_value = perp_notional_position * perp_maint_asset_weight
perp_init_health_base_value = perp_health_base_value * perp_init_asset_weight
perp_maint_health_base_value = perp_health_base_value * perp_maint_asset_weight
perp_init_health_base_value = (
perp_health_base_value * perp_init_asset_weight
)
perp_maint_health_base_value = (
perp_health_base_value * perp_maint_asset_weight
)
else:
perp_init_value = perp_notional_position * perp_init_liab_weight
perp_maint_value = perp_notional_position * perp_maint_liab_weight
perp_init_health_base_value = perp_health_base_value * perp_init_liab_weight
perp_maint_health_base_value = perp_health_base_value * perp_maint_liab_weight
perp_init_health_base_value = (
perp_health_base_value * perp_init_liab_weight
)
perp_maint_health_base_value = (
perp_health_base_value * perp_maint_liab_weight
)
data = {
"Name": slot.base_instrument.name,
"Symbol": slot.base_instrument.symbol,
@ -655,10 +860,22 @@ class Account(AddressableAccount):
"BaseLocked": base_open_locked,
"QuoteUnsettled": quote_open_unsettled,
"QuoteLocked": quote_open_locked,
"BaseUnsettledInMarginBasket": base_open_unsettled if slot.index < len(self.in_margin_basket) and self.in_margin_basket[slot.index] else Decimal(0),
"BaseLockedInMarginBasket": base_open_locked if slot.index < len(self.in_margin_basket) and self.in_margin_basket[slot.index] else Decimal(0),
"QuoteUnsettledInMarginBasket": quote_open_unsettled if slot.index < len(self.in_margin_basket) and self.in_margin_basket[slot.index] else Decimal(0),
"QuoteLockedInMarginBasket": quote_open_locked if slot.index < len(self.in_margin_basket) and self.in_margin_basket[slot.index] else Decimal(0),
"BaseUnsettledInMarginBasket": base_open_unsettled
if slot.index < len(self.in_margin_basket)
and self.in_margin_basket[slot.index]
else Decimal(0),
"BaseLockedInMarginBasket": base_open_locked
if slot.index < len(self.in_margin_basket)
and self.in_margin_basket[slot.index]
else Decimal(0),
"QuoteUnsettledInMarginBasket": quote_open_unsettled
if slot.index < len(self.in_margin_basket)
and self.in_margin_basket[slot.index]
else Decimal(0),
"QuoteLockedInMarginBasket": quote_open_locked
if slot.index < len(self.in_margin_basket)
and self.in_margin_basket[slot.index]
else Decimal(0),
"PerpPositionSize": perp_position,
"PerpNotionalPositionSize": perp_notional_position,
"PerpValue": perp_value,
@ -686,9 +903,13 @@ class Account(AddressableAccount):
frame: pandas.DataFrame = pandas.DataFrame(asset_data)
return frame
def weighted_assets(self, frame: pandas.DataFrame, weighting_name: str = "") -> typing.Tuple[Decimal, Decimal]:
def weighted_assets(
self, frame: pandas.DataFrame, weighting_name: str = ""
) -> typing.Tuple[Decimal, Decimal]:
non_quote = frame.loc[frame["Symbol"] != self.shared_quote_token.symbol]
quote = frame.loc[frame["Symbol"] == self.shared_quote_token.symbol, "SpotValue"].sum()
quote = frame.loc[
frame["Symbol"] == self.shared_quote_token.symbol, "SpotValue"
].sum()
quote += frame["PerpHealthQuote"].sum()
quote += frame["QuoteUnsettledInMarginBasket"].sum()
@ -702,14 +923,22 @@ class Account(AddressableAccount):
spot_value_key = f"Spot{weighting_name}Value"
perp_value_key = f"Perp{weighting_name}HealthBaseValue"
liabilities += Account.__sum_neg(non_quote, spot_value_key) + Account.__sum_neg(non_quote, perp_value_key)
assets += Account.__sum_pos(non_quote, spot_value_key) + Account.__sum_pos(non_quote, perp_value_key)
liabilities += Account.__sum_neg(non_quote, spot_value_key) + Account.__sum_neg(
non_quote, perp_value_key
)
assets += Account.__sum_pos(non_quote, spot_value_key) + Account.__sum_pos(
non_quote, perp_value_key
)
return assets, liabilities
def unweighted_assets(self, frame: pandas.DataFrame) -> typing.Tuple[Decimal, Decimal]:
def unweighted_assets(
self, frame: pandas.DataFrame
) -> typing.Tuple[Decimal, Decimal]:
non_quote = frame.loc[frame["Symbol"] != self.shared_quote_token.symbol]
quote = frame.loc[frame["Symbol"] == self.shared_quote_token.symbol, "SpotValue"].sum()
quote = frame.loc[
frame["Symbol"] == self.shared_quote_token.symbol, "SpotValue"
].sum()
assets = Decimal(0)
liabilities = Decimal(0)
@ -718,11 +947,15 @@ class Account(AddressableAccount):
else:
liabilities = quote
liabilities += Account.__sum_neg(non_quote, "SpotValue") + non_quote['PerpLiability'].sum()
liabilities += (
Account.__sum_neg(non_quote, "SpotValue") + non_quote["PerpLiability"].sum()
)
assets += Account.__sum_pos(non_quote, "SpotValue") + \
non_quote['PerpAsset'].sum() + \
Account.__sum_pos(non_quote, "QuoteUnsettled")
assets += (
Account.__sum_pos(non_quote, "SpotValue")
+ non_quote["PerpAsset"].sum()
+ Account.__sum_pos(non_quote, "QuoteUnsettled")
)
return assets, liabilities
@ -770,9 +1003,13 @@ class Account(AddressableAccount):
info = f"'{self.info}'" if self.info else "(un-named)"
shared_quote: str = f"{self.shared_quote}".replace("\n", "\n ")
slot_count = len(self.base_slots)
slots = "\n ".join([f"{item}".replace("\n", "\n ") for item in self.base_slots])
slots = "\n ".join(
[f"{item}".replace("\n", "\n ") for item in self.base_slots]
)
symbols: typing.Sequence[str] = [slot.base_instrument.symbol for slot in self.base_slots]
symbols: typing.Sequence[str] = [
slot.base_instrument.symbol for slot in self.base_slots
]
in_margin_basket = ", ".join(symbols) or "None"
return f"""« Account {info}, {self.version} [{self.address}]
{self.meta_data}

View File

@ -25,9 +25,20 @@ from .version import Version
# Encapsulates the Serum AccountFlags data.
#
class AccountFlags:
def __init__(self, version: Version, initialized: bool, market: bool, open_orders: bool,
request_queue: bool, event_queue: bool, bids: bool, asks: bool, disabled: bool) -> None:
def __init__(
self,
version: Version,
initialized: bool,
market: bool,
open_orders: bool,
request_queue: bool,
event_queue: bool,
bids: bool,
asks: bool,
disabled: bool,
) -> None:
self._logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.version: Version = version
self.initialized: bool = initialized
@ -41,9 +52,17 @@ class AccountFlags:
@staticmethod
def from_layout(layout: typing.Any) -> "AccountFlags":
return AccountFlags(Version.UNSPECIFIED, layout.initialized, layout.market,
layout.open_orders, layout.request_queue, layout.event_queue,
layout.bids, layout.asks, layout.disabled)
return AccountFlags(
Version.UNSPECIFIED,
layout.initialized,
layout.market,
layout.open_orders,
layout.request_queue,
layout.event_queue,
layout.bids,
layout.asks,
layout.disabled,
)
def __str__(self) -> str:
flags: typing.List[typing.Optional[str]] = []

View File

@ -31,7 +31,15 @@ from .encoding import decode_binary, encode_binary
# # 🥭 AccountInfo class
#
class AccountInfo:
def __init__(self, address: PublicKey, executable: bool, lamports: Decimal, owner: PublicKey, rent_epoch: Decimal, data: bytes) -> None:
def __init__(
self,
address: PublicKey,
executable: bool,
lamports: Decimal,
owner: PublicKey,
rent_epoch: Decimal,
data: bytes,
) -> None:
self._logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.address: PublicKey = address
self.executable: bool = executable
@ -54,7 +62,7 @@ class AccountInfo:
"lamports": str(self.lamports),
"owner": str(self.owner),
"rent_epoch": str(self.rent_epoch),
"data": encode_binary(self.data)
"data": encode_binary(self.data),
}
with open(filename, "w") as json_file:
json.dump(data, json_file, indent=4)
@ -91,7 +99,9 @@ class AccountInfo:
return AccountInfo(address, executable, lamports, owner, rent_epoch, data)
@staticmethod
def load_multiple(context: Context, addresses: typing.Sequence[PublicKey]) -> typing.List["AccountInfo"]:
def load_multiple(
context: Context, addresses: typing.Sequence[PublicKey]
) -> typing.List["AccountInfo"]:
# This is a tricky one to get right.
# Some errors this can generate:
# 413 Client Error: Payload Too Large for url
@ -99,18 +109,29 @@ class AccountInfo:
chunk_size: int = int(context.gma_chunk_size)
sleep_between_calls: float = float(context.gma_chunk_pause)
multiple: typing.List[AccountInfo] = []
chunks: typing.Sequence[typing.Sequence[PublicKey]] = AccountInfo._split_list_into_chunks(addresses, chunk_size)
chunks: typing.Sequence[
typing.Sequence[PublicKey]
] = AccountInfo._split_list_into_chunks(addresses, chunk_size)
for counter, chunk in enumerate(chunks):
result: typing.Sequence[typing.Dict[str, typing.Any]] = context.client.get_multiple_accounts([*chunk])
result: typing.Sequence[
typing.Dict[str, typing.Any]
] = context.client.get_multiple_accounts([*chunk])
response_value_list = zip(result, chunk)
multiple += list(map(lambda pair: AccountInfo._from_response_values(pair[0], pair[1]), response_value_list))
multiple += list(
map(
lambda pair: AccountInfo._from_response_values(pair[0], pair[1]),
response_value_list,
)
)
if (sleep_between_calls > 0.0) and (counter < (len(chunks) - 1)):
time.sleep(sleep_between_calls)
return multiple
@staticmethod
def _from_response_values(response_values: typing.Dict[str, typing.Any], address: PublicKey) -> "AccountInfo":
def _from_response_values(
response_values: typing.Dict[str, typing.Any], address: PublicKey
) -> "AccountInfo":
executable = bool(response_values["executable"])
lamports = Decimal(response_values["lamports"])
owner = PublicKey(response_values["owner"])
@ -123,11 +144,13 @@ class AccountInfo:
return AccountInfo._from_response_values(response["result"]["value"], address)
@staticmethod
def _split_list_into_chunks(to_chunk: typing.Sequence[typing.Any], chunk_size: int = 100) -> typing.Sequence[typing.Sequence[typing.Any]]:
def _split_list_into_chunks(
to_chunk: typing.Sequence[typing.Any], chunk_size: int = 100
) -> typing.Sequence[typing.Sequence[typing.Any]]:
chunks = []
start = 0
while start < len(to_chunk):
chunk = to_chunk[start:start + chunk_size]
chunk = to_chunk[start : start + chunk_size]
chunks += [chunk]
start += chunk_size
return chunks

View File

@ -42,22 +42,30 @@ from .tokenbank import NodeBank, RootBank, TokenBank
# Given a `Context` and an account type, returns a function that can take an `AccountInfo` and
# return one of our objects.
#
def build_account_info_converter(context: Context, account_type: str) -> typing.Callable[[AccountInfo], AddressableAccount]:
def build_account_info_converter(
context: Context, account_type: str
) -> typing.Callable[[AccountInfo], AddressableAccount]:
account_type_upper = account_type.upper()
if account_type_upper == "GROUP":
return lambda account_info: Group.parse_with_context(context, account_info)
elif account_type_upper == "ACCOUNT":
def account_loader(account_info: AccountInfo) -> Account:
layout_account = layouts.MANGO_ACCOUNT.parse(account_info.data)
group_address = layout_account.group
group: Group = Group.load(context, group_address)
cache: Cache = group.fetch_cache(context)
return Account.parse(account_info, group, cache)
return account_loader
elif account_type_upper == "OPENORDERS":
return lambda account_info: OpenOrders.parse(account_info, Decimal(6), Decimal(6))
return lambda account_info: OpenOrders.parse(
account_info, Decimal(6), Decimal(6)
)
elif account_type_upper == "PERPEVENTQUEUE":
return lambda account_info: PerpEventQueue.parse(account_info, NullLotSizeConverter())
return lambda account_info: PerpEventQueue.parse(
account_info, NullLotSizeConverter()
)
elif account_type_upper == "SERUMEVENTQUEUE":
return lambda account_info: SerumEventQueue.parse(account_info)
elif account_type_upper == "CACHE":
@ -67,21 +75,30 @@ def build_account_info_converter(context: Context, account_type: str) -> typing.
elif account_type_upper == "NODEBANK":
return lambda account_info: NodeBank.parse(account_info)
elif account_type_upper == "PERPMARKETDETAILS":
def perp_market_details_loader(account_info: AccountInfo) -> PerpMarketDetails:
layout_perp_market_details = layouts.PERP_MARKET.parse(account_info.data)
group_address = layout_perp_market_details.group
group: Group = Group.load(context, group_address)
return PerpMarketDetails.parse(account_info, group)
return perp_market_details_loader
elif account_type_upper == "PERPORDERBOOKSIDE":
class __FakePerpMarketDetails(PerpMarketDetails):
def __init__(self) -> None:
self.base_instrument = Instrument("UNKNOWNBASE", "Unknown Base", Decimal(0))
self.quote_token = TokenBank(Token("UNKNOWNQUOTE", "Unknown Quote",
Decimal(0), PublicKey(0)), PublicKey(0))
self.base_instrument = Instrument(
"UNKNOWNBASE", "Unknown Base", Decimal(0)
)
self.quote_token = TokenBank(
Token("UNKNOWNQUOTE", "Unknown Quote", Decimal(0), PublicKey(0)),
PublicKey(0),
)
self.base_lot_size = Decimal(1)
self.quote_lot_size = Decimal(1)
return lambda account_info: PerpOrderBookSide.parse(account_info, __FakePerpMarketDetails())
return lambda account_info: PerpOrderBookSide.parse(
account_info, __FakePerpMarketDetails()
)
raise Exception(f"Could not find AccountInfo converter for type {account_type}.")

View File

@ -20,7 +20,10 @@ from decimal import Decimal
from .account import AccountSlot
from .cache import PerpMarketCache, MarketCache, RootBankCache
from .calculators.unsettledfundingcalculator import calculate_unsettled_funding, UnsettledFundingParams
from .calculators.unsettledfundingcalculator import (
calculate_unsettled_funding,
UnsettledFundingParams,
)
from .group import Group
from .instrumentvalue import InstrumentValue
from .lotsizeconverter import LotSizeConverter
@ -33,7 +36,9 @@ from .token import Instrument, Token
#
# `_token_values_from_open_orders()` builds InstrumentValue objects from an OpenOrders object.
#
def _token_values_from_open_orders(base_token: Token, quote_token: Token, spot_open_orders: typing.Sequence[OpenOrders]) -> typing.Tuple[InstrumentValue, InstrumentValue, InstrumentValue, InstrumentValue]:
def _token_values_from_open_orders(
base_token: Token, quote_token: Token, spot_open_orders: typing.Sequence[OpenOrders]
) -> typing.Tuple[InstrumentValue, InstrumentValue, InstrumentValue, InstrumentValue]:
base_token_free: Decimal = Decimal(0)
base_token_total: Decimal = Decimal(0)
quote_token_free: Decimal = Decimal(0)
@ -45,10 +50,12 @@ def _token_values_from_open_orders(base_token: Token, quote_token: Token, spot_o
quote_token_free += open_orders.quote_token_free
quote_token_total += open_orders.quote_token_total
return (InstrumentValue(base_token, base_token_free),
InstrumentValue(base_token, base_token_total),
InstrumentValue(quote_token, quote_token_free),
InstrumentValue(quote_token, quote_token_total))
return (
InstrumentValue(base_token, base_token_free),
InstrumentValue(base_token, base_token_total),
InstrumentValue(quote_token, quote_token_free),
InstrumentValue(quote_token, quote_token_total),
)
# # 🥭 AccountInstrumentValues class
@ -56,7 +63,27 @@ def _token_values_from_open_orders(base_token: Token, quote_token: Token, spot_o
# `AccountInstrumentValues` gathers basket items together instead of separate arrays.
#
class AccountInstrumentValues:
def __init__(self, base_token: Instrument, quote_token: Token, raw_deposit: Decimal, deposit: InstrumentValue, raw_borrow: Decimal, borrow: InstrumentValue, base_token_free: InstrumentValue, base_token_total: InstrumentValue, quote_token_free: InstrumentValue, quote_token_total: InstrumentValue, perp_base_position: InstrumentValue, raw_perp_quote_position: Decimal, raw_taker_quote: Decimal, bids_quantity: InstrumentValue, asks_quantity: InstrumentValue, long_settled_funding: Decimal, short_settled_funding: Decimal, lot_size_converter: LotSizeConverter) -> None:
def __init__(
self,
base_token: Instrument,
quote_token: Token,
raw_deposit: Decimal,
deposit: InstrumentValue,
raw_borrow: Decimal,
borrow: InstrumentValue,
base_token_free: InstrumentValue,
base_token_total: InstrumentValue,
quote_token_free: InstrumentValue,
quote_token_total: InstrumentValue,
perp_base_position: InstrumentValue,
raw_perp_quote_position: Decimal,
raw_taker_quote: Decimal,
bids_quantity: InstrumentValue,
asks_quantity: InstrumentValue,
long_settled_funding: Decimal,
short_settled_funding: Decimal,
lot_size_converter: LotSizeConverter,
) -> None:
self.base_token: Instrument = base_token
self.quote_token: Token = quote_token
self.raw_deposit: Decimal = raw_deposit
@ -102,16 +129,24 @@ class AccountInstrumentValues:
if isinstance(self.base_token, Token):
return PricedAccountInstrumentValues(self, market_cache)
null_root_bank = RootBankCache(Decimal(1), Decimal(1), datetime.now())
market_cache_with_null_root_bank = MarketCache(market_cache.price, null_root_bank, market_cache.perp_market)
market_cache_with_null_root_bank = MarketCache(
market_cache.price, null_root_bank, market_cache.perp_market
)
return PricedAccountInstrumentValues(self, market_cache_with_null_root_bank)
@staticmethod
def from_account_basket_base_token(account_slot: AccountSlot, open_orders_by_address: typing.Dict[str, OpenOrders], group: Group) -> "AccountInstrumentValues":
def from_account_basket_base_token(
account_slot: AccountSlot,
open_orders_by_address: typing.Dict[str, OpenOrders],
group: Group,
) -> "AccountInstrumentValues":
base_token: Instrument = account_slot.base_instrument
quote_token: Token = Token.ensure(account_slot.quote_token_bank.token)
perp_account: typing.Optional[PerpAccount] = account_slot.perp_account
if perp_account is None:
raise Exception(f"No perp account for basket token {account_slot.base_instrument.symbol}")
raise Exception(
f"No perp account for basket token {account_slot.base_instrument.symbol}"
)
base_token_free: InstrumentValue = InstrumentValue(base_token, Decimal(0))
base_token_total: InstrumentValue = InstrumentValue(base_token, Decimal(0))
@ -119,23 +154,63 @@ class AccountInstrumentValues:
quote_token_total: InstrumentValue = InstrumentValue(quote_token, Decimal(0))
if account_slot.spot_open_orders is not None:
open_orders: typing.Sequence[OpenOrders] = [
open_orders_by_address[str(account_slot.spot_open_orders)]]
base_token_free, base_token_total, quote_token_free, quote_token_total = _token_values_from_open_orders(
Token.ensure(base_token), Token.ensure(quote_token), open_orders)
open_orders_by_address[str(account_slot.spot_open_orders)]
]
(
base_token_free,
base_token_total,
quote_token_free,
quote_token_total,
) = _token_values_from_open_orders(
Token.ensure(base_token), Token.ensure(quote_token), open_orders
)
lot_size_converter: LotSizeConverter = perp_account.lot_size_converter
perp_base_position: InstrumentValue = perp_account.base_token_value
perp_quote_position: Decimal = perp_account.quote_position_raw
long_settled_funding: Decimal = perp_account.long_settled_funding / lot_size_converter.quote_lot_size
short_settled_funding: Decimal = perp_account.short_settled_funding / lot_size_converter.quote_lot_size
long_settled_funding: Decimal = (
perp_account.long_settled_funding / lot_size_converter.quote_lot_size
)
short_settled_funding: Decimal = (
perp_account.short_settled_funding / lot_size_converter.quote_lot_size
)
taker_quote: Decimal = perp_account.taker_quote * lot_size_converter.quote_lot_size
bids_quantity: InstrumentValue = InstrumentValue(base_token, base_token.shift_to_decimals(
perp_account.bids_quantity * lot_size_converter.base_lot_size))
asks_quantity: InstrumentValue = InstrumentValue(base_token, base_token.shift_to_decimals(
perp_account.asks_quantity * lot_size_converter.base_lot_size))
taker_quote: Decimal = (
perp_account.taker_quote * lot_size_converter.quote_lot_size
)
bids_quantity: InstrumentValue = InstrumentValue(
base_token,
base_token.shift_to_decimals(
perp_account.bids_quantity * lot_size_converter.base_lot_size
),
)
asks_quantity: InstrumentValue = InstrumentValue(
base_token,
base_token.shift_to_decimals(
perp_account.asks_quantity * lot_size_converter.base_lot_size
),
)
return AccountInstrumentValues(base_token, quote_token, account_slot.raw_deposit, account_slot.deposit, account_slot.raw_borrow, account_slot.borrow, base_token_free, base_token_total, quote_token_free, quote_token_total, perp_base_position, perp_quote_position, taker_quote, bids_quantity, asks_quantity, long_settled_funding, short_settled_funding, lot_size_converter)
return AccountInstrumentValues(
base_token,
quote_token,
account_slot.raw_deposit,
account_slot.deposit,
account_slot.raw_borrow,
account_slot.borrow,
base_token_free,
base_token_total,
quote_token_free,
quote_token_total,
perp_base_position,
perp_quote_position,
taker_quote,
bids_quantity,
asks_quantity,
long_settled_funding,
short_settled_funding,
lot_size_converter,
)
def __str__(self) -> str:
return f"""« AccountInstrumentValues {self.base_token.symbol}
@ -158,53 +233,107 @@ class AccountInstrumentValues:
class PricedAccountInstrumentValues(AccountInstrumentValues):
def __init__(self, original_account_token_values: AccountInstrumentValues, market_cache: MarketCache) -> None:
def __init__(
self,
original_account_token_values: AccountInstrumentValues,
market_cache: MarketCache,
) -> None:
price: InstrumentValue = market_cache.adjusted_price(
original_account_token_values.base_token, original_account_token_values.quote_token)
original_account_token_values.base_token,
original_account_token_values.quote_token,
)
if market_cache.root_bank is None:
raise Exception(f"No root bank for token {original_account_token_values.base_token} in {market_cache}")
raise Exception(
f"No root bank for token {original_account_token_values.base_token} in {market_cache}"
)
deposit_value: Decimal = original_account_token_values.raw_deposit * market_cache.root_bank.deposit_index * price.value
shifted_deposit_value: Decimal = original_account_token_values.quote_token.shift_to_decimals(deposit_value)
deposit: InstrumentValue = InstrumentValue(original_account_token_values.quote_token, shifted_deposit_value)
deposit_value: Decimal = (
original_account_token_values.raw_deposit
* market_cache.root_bank.deposit_index
* price.value
)
shifted_deposit_value: Decimal = (
original_account_token_values.quote_token.shift_to_decimals(deposit_value)
)
deposit: InstrumentValue = InstrumentValue(
original_account_token_values.quote_token, shifted_deposit_value
)
borrow_value: Decimal = original_account_token_values.raw_borrow * market_cache.root_bank.borrow_index * price.value
shifted_borrow_value: Decimal = original_account_token_values.quote_token.shift_to_decimals(borrow_value)
borrow: InstrumentValue = InstrumentValue(original_account_token_values.quote_token, shifted_borrow_value)
borrow_value: Decimal = (
original_account_token_values.raw_borrow
* market_cache.root_bank.borrow_index
* price.value
)
shifted_borrow_value: Decimal = (
original_account_token_values.quote_token.shift_to_decimals(borrow_value)
)
borrow: InstrumentValue = InstrumentValue(
original_account_token_values.quote_token, shifted_borrow_value
)
base_token_free: InstrumentValue = original_account_token_values.base_token_free * price
base_token_total: InstrumentValue = original_account_token_values.base_token_total * price
base_token_free: InstrumentValue = (
original_account_token_values.base_token_free * price
)
base_token_total: InstrumentValue = (
original_account_token_values.base_token_total * price
)
perp_base_position: InstrumentValue = original_account_token_values.perp_base_position * price
perp_base_position: InstrumentValue = (
original_account_token_values.perp_base_position * price
)
super().__init__(original_account_token_values.base_token, original_account_token_values.quote_token,
original_account_token_values.raw_deposit, deposit,
original_account_token_values.raw_borrow, borrow, base_token_free, base_token_total,
original_account_token_values.quote_token_free,
original_account_token_values.quote_token_total,
perp_base_position, original_account_token_values.raw_perp_quote_position,
original_account_token_values.raw_taker_quote,
original_account_token_values.bids_quantity, original_account_token_values.asks_quantity,
original_account_token_values.long_settled_funding, original_account_token_values.short_settled_funding,
original_account_token_values.lot_size_converter)
self.original_account_token_values: AccountInstrumentValues = original_account_token_values
super().__init__(
original_account_token_values.base_token,
original_account_token_values.quote_token,
original_account_token_values.raw_deposit,
deposit,
original_account_token_values.raw_borrow,
borrow,
base_token_free,
base_token_total,
original_account_token_values.quote_token_free,
original_account_token_values.quote_token_total,
perp_base_position,
original_account_token_values.raw_perp_quote_position,
original_account_token_values.raw_taker_quote,
original_account_token_values.bids_quantity,
original_account_token_values.asks_quantity,
original_account_token_values.long_settled_funding,
original_account_token_values.short_settled_funding,
original_account_token_values.lot_size_converter,
)
self.original_account_token_values: AccountInstrumentValues = (
original_account_token_values
)
self.price: InstrumentValue = price
self.perp_market_cache: typing.Optional[PerpMarketCache] = market_cache.perp_market
self.perp_market_cache: typing.Optional[
PerpMarketCache
] = market_cache.perp_market
perp_quote_position: InstrumentValue = InstrumentValue(
original_account_token_values.quote_token, original_account_token_values.raw_perp_quote_position)
original_account_token_values.quote_token,
original_account_token_values.raw_perp_quote_position,
)
if market_cache.perp_market is not None:
original: AccountInstrumentValues = original_account_token_values
long_funding: Decimal = market_cache.perp_market.long_funding / original.lot_size_converter.quote_lot_size
short_funding: Decimal = market_cache.perp_market.short_funding / original.lot_size_converter.quote_lot_size
unsettled_funding: InstrumentValue = calculate_unsettled_funding(UnsettledFundingParams(
quote_token=original.quote_token,
base_position=original.perp_base_position,
long_funding=long_funding,
long_settled_funding=original.long_settled_funding,
short_funding=short_funding,
short_settled_funding=original.short_settled_funding
))
long_funding: Decimal = (
market_cache.perp_market.long_funding
/ original.lot_size_converter.quote_lot_size
)
short_funding: Decimal = (
market_cache.perp_market.short_funding
/ original.lot_size_converter.quote_lot_size
)
unsettled_funding: InstrumentValue = calculate_unsettled_funding(
UnsettledFundingParams(
quote_token=original.quote_token,
base_position=original.perp_base_position,
long_funding=long_funding,
long_settled_funding=original.long_settled_funding,
short_funding=short_funding,
short_settled_funding=original.short_settled_funding,
)
)
perp_quote_position -= unsettled_funding
self.perp_quote_position: InstrumentValue = perp_quote_position
@ -218,14 +347,24 @@ class PricedAccountInstrumentValues(AccountInstrumentValues):
return self.perp_base_position - (self.asks_quantity * self.price)
def if_worst_execution(self) -> typing.Tuple[InstrumentValue, InstrumentValue]:
taker_quote: InstrumentValue = InstrumentValue(self.perp_quote_position.token, self.raw_taker_quote)
taker_quote: InstrumentValue = InstrumentValue(
self.perp_quote_position.token, self.raw_taker_quote
)
if abs(self.if_all_bids_executed.value) > abs(self.if_all_asks_executed.value):
base_position = self.if_all_bids_executed
quote_position = self.perp_quote_position + taker_quote - (self.bids_quantity * self.price)
quote_position = (
self.perp_quote_position
+ taker_quote
- (self.bids_quantity * self.price)
)
else:
base_position = self.if_all_asks_executed
quote_position = self.perp_quote_position + taker_quote + (self.asks_quantity * self.price)
quote_position = (
self.perp_quote_position
+ taker_quote
+ (self.asks_quantity * self.price)
)
return base_position, quote_position

View File

@ -43,17 +43,26 @@ from .liquidatablereport import LiquidatableReport
# is just the `liquidate()` method.
#
class AccountLiquidator(metaclass=abc.ABCMeta):
def __init__(self) -> None:
self._logger: logging.Logger = logging.getLogger(self.__class__.__name__)
@abc.abstractmethod
def prepare_instructions(self, liquidatable_report: LiquidatableReport) -> typing.Sequence[TransactionInstruction]:
raise NotImplementedError("AccountLiquidator.prepare_instructions() is not implemented on the base type.")
def prepare_instructions(
self, liquidatable_report: LiquidatableReport
) -> typing.Sequence[TransactionInstruction]:
raise NotImplementedError(
"AccountLiquidator.prepare_instructions() is not implemented on the base type."
)
@abc.abstractmethod
def liquidate(self, liquidatable_report: LiquidatableReport) -> typing.Optional[typing.Sequence[str]]:
raise NotImplementedError("AccountLiquidator.liquidate() is not implemented on the base type.")
def liquidate(
self, liquidatable_report: LiquidatableReport
) -> typing.Optional[typing.Sequence[str]]:
raise NotImplementedError(
"AccountLiquidator.liquidate() is not implemented on the base type."
)
# # NullAccountLiquidator class
@ -61,13 +70,20 @@ class AccountLiquidator(metaclass=abc.ABCMeta):
# A 'null', 'no-op', 'dry run' implementation of the `AccountLiquidator` class.
#
class NullAccountLiquidator(AccountLiquidator):
def __init__(self) -> None:
super().__init__()
def prepare_instructions(self, liquidatable_report: LiquidatableReport) -> typing.Sequence[TransactionInstruction]:
def prepare_instructions(
self, liquidatable_report: LiquidatableReport
) -> typing.Sequence[TransactionInstruction]:
return []
def liquidate(self, liquidatable_report: LiquidatableReport) -> typing.Optional[typing.Sequence[str]]:
self._logger.info(f"Skipping liquidation of account [{liquidatable_report.account.address}]")
def liquidate(
self, liquidatable_report: LiquidatableReport
) -> typing.Optional[typing.Sequence[str]]:
self._logger.info(
f"Skipping liquidation of account [{liquidatable_report.account.address}]"
)
return None

View File

@ -84,7 +84,9 @@ class ScoutReport:
if len(text_list) == 0:
return "None"
padding = "\n "
return padding.join(map(lambda text: text.replace("\n", padding), text_list))
return padding.join(
map(lambda text: text.replace("\n", padding), text_list)
)
error_text = _pad(self.errors)
warning_text = _pad(self.warnings)
@ -120,16 +122,23 @@ class ScoutReport:
# Passing all checks here with no errors will be a precondition on liquidator startup.
#
class AccountScout:
def __init__(self) -> None:
pass
def require_account_prepared_for_group(self, context: Context, group: Group, account_address: PublicKey) -> None:
def require_account_prepared_for_group(
self, context: Context, group: Group, account_address: PublicKey
) -> None:
report = self.verify_account_prepared_for_group(context, group, account_address)
if report.has_errors:
raise Exception(f"Account '{account_address}' is not prepared for group '{group.address}':\n\n{report}")
raise Exception(
f"Account '{account_address}' is not prepared for group '{group.address}':\n\n{report}"
)
def verify_account_prepared_for_group(self, context: Context, group: Group, account_address: PublicKey) -> ScoutReport:
def verify_account_prepared_for_group(
self, context: Context, group: Group, account_address: PublicKey
) -> ScoutReport:
report = ScoutReport(account_address)
# First of all, the account must actually exist. If it doesn't, just return early.
@ -147,18 +156,23 @@ class AccountScout:
for basket_token in group.tokens:
if isinstance(basket_token.token, Token):
token_accounts = TokenAccount.fetch_all_for_owner_and_token(
context, account_address, basket_token.token)
context, account_address, basket_token.token
)
if len(token_accounts) == 0:
report.add_error(
f"Account '{account_address}' has no account for token '{basket_token.token.name}'.")
f"Account '{account_address}' has no account for token '{basket_token.token.name}'."
)
else:
report.add_detail(
f"Account '{account_address}' has {len(token_accounts)} {basket_token.token.name} token account(s): {[ta.address for ta in token_accounts]}")
f"Account '{account_address}' has {len(token_accounts)} {basket_token.token.name} token account(s): {[ta.address for ta in token_accounts]}"
)
# May have one or more Mango Markets margin account, but it's optional for liquidating
accounts = Account.load_all_for_owner(context, account_address, group)
if len(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:
for account in accounts:
report.add_detail(f"Margin account: {account}")

View File

@ -35,23 +35,29 @@ def setup_logging(log_level: int, suppress_timestamp: bool) -> None:
log_record_format = "%(level_emoji)s %(name)-12.12s %(message)s"
# Make logging a little more verbose than the default.
logging.basicConfig(level=logging.INFO, datefmt="%Y-%m-%d %H:%M:%S", format=log_record_format)
logging.basicConfig(
level=logging.INFO, datefmt="%Y-%m-%d %H:%M:%S", format=log_record_format
)
# Stop libraries outputting lots of information unless it's a warning or worse.
logging.getLogger("requests").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("solanaweb3").setLevel(logging.WARNING)
default_log_record_factory: typing.Callable[..., logging.LogRecord] = logging.getLogRecordFactory()
default_log_record_factory: typing.Callable[
..., logging.LogRecord
] = logging.getLogRecordFactory()
log_levels: typing.Dict[int, str] = {
logging.CRITICAL: "🛑",
logging.ERROR: "🚨",
logging.WARNING: "",
logging.INFO: "",
logging.DEBUG: "🐛"
logging.DEBUG: "🐛",
}
def _emojified_record_factory(*args: typing.Any, **kwargs: typing.Any) -> logging.LogRecord:
def _emojified_record_factory(
*args: typing.Any, **kwargs: typing.Any
) -> logging.LogRecord:
record = default_log_record_factory(*args, **kwargs)
# Here's where we add our own format keywords.
setattr(record, "level_emoji", log_levels[record.levelno])
@ -66,13 +72,28 @@ def setup_logging(log_level: int, suppress_timestamp: bool) -> None:
#
# This function parses CLI arguments and sets up common logging for all commands.
#
def parse_args(parser: argparse.ArgumentParser, logging_default: int = logging.INFO) -> argparse.Namespace:
parser.add_argument("--log-level", default=logging_default, type=lambda level: typing.cast(object, getattr(logging, level)),
help="level of verbosity to log (possible values: DEBUG, INFO, WARNING, ERROR, CRITICAL)")
parser.add_argument("--log-suppress-timestamp", default=False, action="store_true",
help="Suppress timestamp in log output (useful for systems that supply their own timestamp on log messages)")
parser.add_argument("--output-format", type=OutputFormat, default=OutputFormat.TEXT,
choices=list(OutputFormat), help="output format - can be TEXT (the default) or JSON")
def parse_args(
parser: argparse.ArgumentParser, logging_default: int = logging.INFO
) -> argparse.Namespace:
parser.add_argument(
"--log-level",
default=logging_default,
type=lambda level: typing.cast(object, getattr(logging, level)),
help="level of verbosity to log (possible values: DEBUG, INFO, WARNING, ERROR, CRITICAL)",
)
parser.add_argument(
"--log-suppress-timestamp",
default=False,
action="store_true",
help="Suppress timestamp in log output (useful for systems that supply their own timestamp on log messages)",
)
parser.add_argument(
"--output-format",
type=OutputFormat,
default=OutputFormat.TEXT,
choices=list(OutputFormat),
help="output format - can be TEXT (the default) or JSON",
)
args: argparse.Namespace = parser.parse_args()
output_formatter.format = args.output_format
@ -87,7 +108,9 @@ def parse_args(parser: argparse.ArgumentParser, logging_default: int = logging.I
all_arguments += [f" --{arg} {getattr(args, arg)}"]
all_arguments.sort()
all_arguments_rendered = "\n".join(all_arguments)
logging.debug(f"{os.path.basename(sys.argv[0])} arguments:\n{all_arguments_rendered}")
logging.debug(
f"{os.path.basename(sys.argv[0])} arguments:\n{all_arguments_rendered}"
)
logging.debug(f"Version: {version()}")

View File

@ -26,7 +26,13 @@ from .token import Token
# # 🥭 BalanceSheet class
#
class BalanceSheet:
def __init__(self, token: Token, liabilities: Decimal, settled_assets: Decimal, unsettled_assets: Decimal) -> None:
def __init__(
self,
token: Token,
liabilities: Decimal,
settled_assets: Decimal,
unsettled_assets: Decimal,
) -> None:
self._logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.token: Token = token
self.liabilities: Decimal = liabilities
@ -48,7 +54,10 @@ class BalanceSheet:
return self.assets / self.liabilities
@staticmethod
def report(values: typing.Sequence["BalanceSheet"], reporter: typing.Callable[[str], None] = output) -> None:
def report(
values: typing.Sequence["BalanceSheet"],
reporter: typing.Callable[[str], None] = output,
) -> None:
for value in values:
reporter(str(value))

View File

@ -56,7 +56,9 @@ class PriceCache:
# `RootBankCache` stores cached details of deposits and borrows.
#
class RootBankCache:
def __init__(self, deposit_index: Decimal, borrow_index: Decimal, last_update: datetime) -> None:
def __init__(
self, deposit_index: Decimal, borrow_index: Decimal, last_update: datetime
) -> None:
self.deposit_index: Decimal = deposit_index
self.borrow_index: Decimal = borrow_index
self.last_update: datetime = last_update
@ -65,7 +67,9 @@ class RootBankCache:
def from_layout(layout: typing.Any) -> typing.Optional["RootBankCache"]:
if layout.last_update.timestamp() == 0:
return None
return RootBankCache(layout.deposit_index, layout.borrow_index, layout.last_update)
return RootBankCache(
layout.deposit_index, layout.borrow_index, layout.last_update
)
def __str__(self) -> str:
return f"« RootBankCache [{self.last_update}] {self.deposit_index:,.20f} / {self.borrow_index:,.20f} »"
@ -79,7 +83,9 @@ class RootBankCache:
# `PerpMarketCache` stores cached details of long and short funding.
#
class PerpMarketCache:
def __init__(self, long_funding: Decimal, short_funding: Decimal, last_update: datetime) -> None:
def __init__(
self, long_funding: Decimal, short_funding: Decimal, last_update: datetime
) -> None:
self.long_funding: Decimal = long_funding
self.short_funding: Decimal = short_funding
self.last_update: datetime = last_update
@ -88,7 +94,9 @@ class PerpMarketCache:
def from_layout(layout: typing.Any) -> typing.Optional["PerpMarketCache"]:
if layout.last_update.timestamp() == 0:
return None
return PerpMarketCache(layout.long_funding, layout.short_funding, layout.last_update)
return PerpMarketCache(
layout.long_funding, layout.short_funding, layout.last_update
)
def __str__(self) -> str:
return f"« PerpMarketCache [{self.last_update}] {self.long_funding:,.20f} / {self.short_funding:,.20f} »"
@ -102,7 +110,12 @@ class PerpMarketCache:
# `MarketCache` stores cached details of price, root bank, and perp market, for a particular market.
#
class MarketCache:
def __init__(self, price: typing.Optional[PriceCache], root_bank: typing.Optional[RootBankCache], perp_market: typing.Optional[PerpMarketCache]) -> None:
def __init__(
self,
price: typing.Optional[PriceCache],
root_bank: typing.Optional[RootBankCache],
perp_market: typing.Optional[PerpMarketCache],
) -> None:
self.price: typing.Optional[PriceCache] = price
self.root_bank: typing.Optional[RootBankCache] = root_bank
self.perp_market: typing.Optional[PerpMarketCache] = perp_market
@ -113,12 +126,14 @@ class MarketCache:
return InstrumentValue(quote_token, Decimal(1))
if self.price is None:
raise Exception(f"Could not find price index of basket token {token.symbol}.")
raise Exception(
f"Could not find price index of basket token {token.symbol}."
)
price: Decimal = self.price.price
decimals_difference = token.decimals - quote_token.decimals
if decimals_difference != 0:
adjustment = 10 ** decimals_difference
adjustment = 10**decimals_difference
price = price * adjustment
return InstrumentValue(quote_token, price)
@ -139,36 +154,58 @@ class MarketCache:
# `Cache` stores cache details of prices, root banks and perp markets.
#
class Cache(AddressableAccount):
def __init__(self, account_info: AccountInfo, version: Version, meta_data: Metadata,
price_cache: typing.Sequence[typing.Optional[PriceCache]],
root_bank_cache: typing.Sequence[typing.Optional[RootBankCache]],
perp_market_cache: typing.Sequence[typing.Optional[PerpMarketCache]]) -> None:
def __init__(
self,
account_info: AccountInfo,
version: Version,
meta_data: Metadata,
price_cache: typing.Sequence[typing.Optional[PriceCache]],
root_bank_cache: typing.Sequence[typing.Optional[RootBankCache]],
perp_market_cache: typing.Sequence[typing.Optional[PerpMarketCache]],
) -> None:
super().__init__(account_info)
self.version: Version = version
self.meta_data: Metadata = meta_data
self.price_cache: typing.Sequence[typing.Optional[PriceCache]] = price_cache
self.root_bank_cache: typing.Sequence[typing.Optional[RootBankCache]] = root_bank_cache
self.perp_market_cache: typing.Sequence[typing.Optional[PerpMarketCache]] = perp_market_cache
self.root_bank_cache: typing.Sequence[
typing.Optional[RootBankCache]
] = root_bank_cache
self.perp_market_cache: typing.Sequence[
typing.Optional[PerpMarketCache]
] = perp_market_cache
@staticmethod
def from_layout(layout: typing.Any, account_info: AccountInfo, version: Version) -> "Cache":
def from_layout(
layout: typing.Any, account_info: AccountInfo, version: Version
) -> "Cache":
meta_data: Metadata = Metadata.from_layout(layout.meta_data)
price_cache: typing.Sequence[typing.Optional[PriceCache]] = list(
map(PriceCache.from_layout, layout.price_cache))
map(PriceCache.from_layout, layout.price_cache)
)
root_bank_cache: typing.Sequence[typing.Optional[RootBankCache]] = list(
map(RootBankCache.from_layout, layout.root_bank_cache))
map(RootBankCache.from_layout, layout.root_bank_cache)
)
perp_market_cache: typing.Sequence[typing.Optional[PerpMarketCache]] = list(
map(PerpMarketCache.from_layout, layout.perp_market_cache))
map(PerpMarketCache.from_layout, layout.perp_market_cache)
)
return Cache(account_info, version, meta_data, price_cache, root_bank_cache, perp_market_cache)
return Cache(
account_info,
version,
meta_data,
price_cache,
root_bank_cache,
perp_market_cache,
)
@staticmethod
def parse(account_info: AccountInfo) -> "Cache":
data = account_info.data
if len(data) != layouts.CACHE.sizeof():
raise Exception(
f"Cache data length ({len(data)}) does not match expected size ({layouts.CACHE.sizeof()})")
f"Cache data length ({len(data)}) does not match expected size ({layouts.CACHE.sizeof()})"
)
layout = layouts.CACHE.parse(data)
return Cache.from_layout(layout, account_info, Version.V1)
@ -181,17 +218,30 @@ class Cache(AddressableAccount):
return Cache.parse(account_info)
def market_cache_for_index(self, index: int) -> MarketCache:
return MarketCache(self.price_cache[index], self.root_bank_cache[index], self.perp_market_cache[index])
return MarketCache(
self.price_cache[index],
self.root_bank_cache[index],
self.perp_market_cache[index],
)
def __str__(self) -> str:
def _render_list(items: typing.Sequence[typing.Any], stub: str) -> typing.Sequence[str]:
def _render_list(
items: typing.Sequence[typing.Any], stub: str
) -> typing.Sequence[str]:
rendered = []
for index, item in enumerate(items):
rendered += [f"{index}: {(item or stub)}".replace("\n", "\n ")]
rendered += [
f"{index}: {(item or stub)}".replace("\n", "\n ")
]
return rendered
prices = "\n ".join(_render_list(self.price_cache, "« No PriceCache »"))
root_banks = "\n ".join(_render_list(self.root_bank_cache, "« No RootBankCache »"))
perp_markets = "\n ".join(_render_list(self.perp_market_cache, "« No PerpMarketCache »"))
root_banks = "\n ".join(
_render_list(self.root_bank_cache, "« No RootBankCache »")
)
perp_markets = "\n ".join(
_render_list(self.perp_market_cache, "« No PerpMarketCache »")
)
return f"""« Cache [{self.version}] {self.address}
{self.meta_data}
Prices:

View File

@ -28,5 +28,13 @@ class CollateralCalculator(metaclass=abc.ABCMeta):
def __init__(self) -> None:
self._logger: logging.Logger = logging.getLogger(self.__class__.__name__)
def calculate(self, account: Account, all_open_orders: typing.Dict[str, OpenOrders], group: Group, cache: Cache) -> InstrumentValue:
raise NotImplementedError("CollateralCalculator.calculate() is not implemented on the base type.")
def calculate(
self,
account: Account,
all_open_orders: typing.Dict[str, OpenOrders],
group: Group,
cache: Cache,
) -> InstrumentValue:
raise NotImplementedError(
"CollateralCalculator.calculate() is not implemented on the base type."
)

View File

@ -20,7 +20,10 @@ import typing
from decimal import Decimal
from ..account import Account, AccountSlot
from ..accountinstrumentvalues import AccountInstrumentValues, PricedAccountInstrumentValues
from ..accountinstrumentvalues import (
AccountInstrumentValues,
PricedAccountInstrumentValues,
)
from ..cache import Cache, MarketCache
from ..context import Context
from ..group import GroupSlotSpotMarket, GroupSlotPerpMarket, GroupSlot, Group
@ -53,9 +56,13 @@ class HealthCalculator:
self.context: Context = context
self.health_type: HealthType = health_type
def _calculate_pessimistic_spot_value(self, values: PricedAccountInstrumentValues) -> typing.Tuple[InstrumentValue, InstrumentValue]:
def _calculate_pessimistic_spot_value(
self, values: PricedAccountInstrumentValues
) -> typing.Tuple[InstrumentValue, InstrumentValue]:
# base total if all bids were executed
if_all_bids_executed: InstrumentValue = values.quote_token_locked + values.base_token_total
if_all_bids_executed: InstrumentValue = (
values.quote_token_locked + values.base_token_total
)
# base total if all asks were executed
if_all_asks_executed: InstrumentValue = values.base_token_free
@ -71,16 +78,27 @@ class HealthCalculator:
quote = values.base_token_locked + values.quote_token_total
return base, quote
def _calculate_pessimistic_perp_value(self, values: PricedAccountInstrumentValues) -> typing.Tuple[InstrumentValue, InstrumentValue]:
def _calculate_pessimistic_perp_value(
self, values: PricedAccountInstrumentValues
) -> typing.Tuple[InstrumentValue, InstrumentValue]:
return values.perp_base_position, values.perp_quote_position
def _calculate_perp_value(self, basket_token: AccountSlot, token_price: InstrumentValue, market_index: int, cache: Cache, unadjustment_factor: Decimal) -> typing.Tuple[Decimal, Decimal]:
def _calculate_perp_value(
self,
basket_token: AccountSlot,
token_price: InstrumentValue,
market_index: int,
cache: Cache,
unadjustment_factor: Decimal,
) -> typing.Tuple[Decimal, Decimal]:
if basket_token.perp_account is None or basket_token.perp_account.empty:
return Decimal(0), Decimal(0)
perp_market_cache = cache.perp_market_cache[market_index]
if perp_market_cache is None:
raise Exception(f"Cache contains no perp market cache for market index {market_index}.")
raise Exception(
f"Cache contains no perp market cache for market index {market_index}."
)
perp_account: PerpAccount = basket_token.perp_account
token: Instrument = basket_token.base_instrument
@ -88,64 +106,94 @@ class HealthCalculator:
quote_lot_size: Decimal = perp_account.lot_size_converter.quote_lot_size
takerQuote: Decimal = perp_account.taker_quote * quote_lot_size
base_position: Decimal = (perp_account.base_position + perp_account.taker_base) * base_lot_size
base_position: Decimal = (
perp_account.base_position + perp_account.taker_base
) * base_lot_size
bids_quantity: Decimal = perp_account.bids_quantity * base_lot_size
asks_quantity: Decimal = perp_account.asks_quantity * base_lot_size
if_all_bids_executed = token.shift_to_decimals(base_position + bids_quantity) * unadjustment_factor
if_all_asks_executed = token.shift_to_decimals(base_position - asks_quantity) * unadjustment_factor
if_all_bids_executed = (
token.shift_to_decimals(base_position + bids_quantity) * unadjustment_factor
)
if_all_asks_executed = (
token.shift_to_decimals(base_position - asks_quantity) * unadjustment_factor
)
if abs(if_all_bids_executed) > abs(if_all_asks_executed):
quote_position = perp_account.quote_position - perp_account.unsettled_funding(perp_market_cache)
full_quote_position = quote_position + takerQuote - (bids_quantity * token_price.value)
quote_position = (
perp_account.quote_position
- perp_account.unsettled_funding(perp_market_cache)
)
full_quote_position = (
quote_position + takerQuote - (bids_quantity * token_price.value)
)
return if_all_bids_executed, full_quote_position
else:
quote_position = perp_account.quote_position - perp_account.unsettled_funding(perp_market_cache)
full_quote_position = quote_position + takerQuote + (asks_quantity * token_price.value)
quote_position = (
perp_account.quote_position
- perp_account.unsettled_funding(perp_market_cache)
)
full_quote_position = (
quote_position + takerQuote + (asks_quantity * token_price.value)
)
return if_all_asks_executed, full_quote_position
def calculate(self, account: Account, open_orders_by_address: typing.Dict[str, OpenOrders], group: Group, cache: Cache) -> Decimal:
def calculate(
self,
account: Account,
open_orders_by_address: typing.Dict[str, OpenOrders],
group: Group,
cache: Cache,
) -> Decimal:
priced_reports: typing.List[PricedAccountInstrumentValues] = []
for asset in account.base_slots:
# if (asset.deposit.value != 0) or (asset.borrow.value != 0) or (asset.net_value.value != 0):
report: AccountInstrumentValues = AccountInstrumentValues.from_account_basket_base_token(
asset, open_orders_by_address, group)
report: AccountInstrumentValues = (
AccountInstrumentValues.from_account_basket_base_token(
asset, open_orders_by_address, group
)
)
# print("report", report)
# price: InstrumentValue = group.token_price_from_cache(cache, report.base_token)
market_cache: MarketCache = group.market_cache_from_cache(cache, report.base_token)
market_cache: MarketCache = group.market_cache_from_cache(
cache, report.base_token
)
# print("Market cache", market_cache)
priced_report: PricedAccountInstrumentValues = report.priced(market_cache)
# print("priced_report", priced_report)
priced_reports += [priced_report]
quote_token_free_in_open_orders: InstrumentValue = InstrumentValue(group.shared_quote_token, Decimal(0))
quote_token_total_in_open_orders: InstrumentValue = InstrumentValue(group.shared_quote_token, Decimal(0))
quote_token_free_in_open_orders: InstrumentValue = InstrumentValue(
group.shared_quote_token, Decimal(0)
)
quote_token_total_in_open_orders: InstrumentValue = InstrumentValue(
group.shared_quote_token, Decimal(0)
)
for priced_report in priced_reports:
quote_token_free_in_open_orders += priced_report.quote_token_free
quote_token_total_in_open_orders += priced_report.quote_token_total
# print("quote_token_free_in_open_orders", quote_token_free_in_open_orders)
# print("quote_token_total_in_open_orders", quote_token_total_in_open_orders)
quote_report: AccountInstrumentValues = AccountInstrumentValues(account.shared_quote_token,
account.shared_quote_token,
account.shared_quote.raw_deposit,
account.shared_quote.deposit,
account.shared_quote.raw_borrow,
account.shared_quote.borrow,
InstrumentValue(
group.shared_quote_token, Decimal(0)),
InstrumentValue(
group.shared_quote_token, Decimal(0)),
quote_token_free_in_open_orders,
quote_token_total_in_open_orders,
InstrumentValue(
group.shared_quote_token, Decimal(0)),
Decimal(0), Decimal(0),
InstrumentValue(
group.shared_quote_token, Decimal(0)),
InstrumentValue(
group.shared_quote_token, Decimal(0)),
Decimal(0), Decimal(0),
NullLotSizeConverter())
quote_report: AccountInstrumentValues = AccountInstrumentValues(
account.shared_quote_token,
account.shared_quote_token,
account.shared_quote.raw_deposit,
account.shared_quote.deposit,
account.shared_quote.raw_borrow,
account.shared_quote.borrow,
InstrumentValue(group.shared_quote_token, Decimal(0)),
InstrumentValue(group.shared_quote_token, Decimal(0)),
quote_token_free_in_open_orders,
quote_token_total_in_open_orders,
InstrumentValue(group.shared_quote_token, Decimal(0)),
Decimal(0),
Decimal(0),
InstrumentValue(group.shared_quote_token, Decimal(0)),
InstrumentValue(group.shared_quote_token, Decimal(0)),
Decimal(0),
Decimal(0),
NullLotSizeConverter(),
)
# print("quote_report", quote_report)
health: Decimal = quote_report.net_value.value
@ -155,9 +203,15 @@ class HealthCalculator:
spot_health = Decimal(0)
spot_market: typing.Optional[GroupSlotSpotMarket] = slot.spot_market
if spot_market is not None:
base_value, quote_value = self._calculate_pessimistic_spot_value(priced_report)
base_value, quote_value = self._calculate_pessimistic_spot_value(
priced_report
)
spot_weight = spot_market.init_asset_weight if base_value > 0 else spot_market.init_liab_weight
spot_weight = (
spot_market.init_asset_weight
if base_value > 0
else spot_market.init_liab_weight
)
spot_health = base_value.value * spot_weight
# print("Weights", base_value.value, "*", spot_weight, spot_health)
@ -165,7 +219,11 @@ class HealthCalculator:
perp_market: typing.Optional[GroupSlotPerpMarket] = slot.perp_market
perp_health: Decimal = Decimal(0)
if perp_market is not None:
perp_weight = perp_market.init_asset_weight if perp_base > 0 else perp_market.init_liab_weight
perp_weight = (
perp_market.init_asset_weight
if perp_base > 0
else perp_market.init_liab_weight
)
perp_health = perp_base.value * perp_weight
health += spot_health

View File

@ -39,7 +39,13 @@ class PerpCollateralCalculator(CollateralCalculator):
# Also from Daffy, same thread, when I said there were two `init_asset_weights`, one for spot and one for perp (https://discord.com/channels/791995070613159966/807051268304273408/882030633940054056):
# yes I think we ignore perps
#
def calculate(self, account: Account, all_open_orders: typing.Dict[str, OpenOrders], group: Group, cache: Cache) -> InstrumentValue:
def calculate(
self,
account: Account,
all_open_orders: typing.Dict[str, OpenOrders],
group: Group,
cache: Cache,
) -> InstrumentValue:
# Quote token calculation:
# total_collateral = deposits[QUOTE_INDEX] * deposit_index - borrows[QUOTE_INDEX] * borrow_index
# Note: the `AccountSlot` in the `Account` already factors the deposit and borrow index.
@ -47,7 +53,9 @@ class PerpCollateralCalculator(CollateralCalculator):
collateral_description = [f"{total:,.8f} USDC"]
for basket_token in account.base_slots:
slot: GroupSlot = group.slot_by_instrument(basket_token.base_instrument)
token_price = group.token_price_from_cache(cache, basket_token.base_instrument)
token_price = group.token_price_from_cache(
cache, basket_token.base_instrument
)
# Not using perp market asset weights yet - stick with spot.
# perp_market: typing.Optional[GroupSlotPerpMarket] = group.perp_markets_by_index[index]
@ -64,19 +72,23 @@ class PerpCollateralCalculator(CollateralCalculator):
perp_market: typing.Optional[GroupSlotPerpMarket] = slot.perp_market
if perp_market is None:
raise Exception(
f"Could not read spot or perp market of token {basket_token.base_instrument.symbol} at index {slot.index} of cache at {cache.address}")
f"Could not read spot or perp market of token {basket_token.base_instrument.symbol} at index {slot.index} of cache at {cache.address}"
)
init_asset_weight = perp_market.init_asset_weight
init_liab_weight = perp_market.init_liab_weight
# Base token calculations:
# total_collateral += prices[i] * (init_asset_weights[i] * deposits[i] * deposit_index - init_liab_weights[i] * borrows[i] * borrow_index)
# Note: the `AccountSlot` in the `Account` already factors the deposit and borrow index.
weighted: Decimal = token_price.value * ((
basket_token.deposit.value * init_asset_weight) - (
basket_token.borrow.value * init_liab_weight))
weighted: Decimal = token_price.value * (
(basket_token.deposit.value * init_asset_weight)
- (basket_token.borrow.value * init_liab_weight)
)
if weighted != 0:
collateral_description += [f"{weighted:,.8f} USDC from {basket_token.base_instrument.symbol}"]
collateral_description += [
f"{weighted:,.8f} USDC from {basket_token.base_instrument.symbol}"
]
total += weighted
self._logger.debug(f"Weighted collateral: {', '.join(collateral_description)}")

View File

@ -28,5 +28,13 @@ class SerumCollateralCalculator(CollateralCalculator):
def __init__(self) -> None:
super().__init__()
def calculate(self, account: Account, all_open_orders: typing.Dict[str, OpenOrders], group: Group, cache: Cache) -> InstrumentValue:
raise NotImplementedError("SerumCollateralCalculator.calculate() is not implemented.")
def calculate(
self,
account: Account,
all_open_orders: typing.Dict[str, OpenOrders],
group: Group,
cache: Cache,
) -> InstrumentValue:
raise NotImplementedError(
"SerumCollateralCalculator.calculate() is not implemented."
)

View File

@ -39,7 +39,13 @@ class SpotCollateralCalculator(CollateralCalculator):
# Also from Daffy, same thread, when I said there were two `init_asset_weights`, one for spot and one for perp (https://discord.com/channels/791995070613159966/807051268304273408/882030633940054056):
# yes I think we ignore perps
#
def calculate(self, account: Account, all_open_orders: typing.Dict[str, OpenOrders], group: Group, cache: Cache) -> InstrumentValue:
def calculate(
self,
account: Account,
all_open_orders: typing.Dict[str, OpenOrders],
group: Group,
cache: Cache,
) -> InstrumentValue:
# Quote token calculation:
# total_collateral = deposits[QUOTE_INDEX] * deposit_index - borrows[QUOTE_INDEX] * borrow_index
# Note: the `AccountSlot` in the `Account` already factors the deposit and borrow index.
@ -47,7 +53,9 @@ class SpotCollateralCalculator(CollateralCalculator):
collateral_description = [f"{total:,.8f} USDC"]
for basket_token in account.base_slots:
slot: GroupSlot = group.slot_by_instrument(basket_token.base_instrument)
token_price = group.token_price_from_cache(cache, basket_token.base_instrument)
token_price = group.token_price_from_cache(
cache, basket_token.base_instrument
)
spot_market: typing.Optional[GroupSlotSpotMarket] = slot.spot_market
init_asset_weight: Decimal
@ -59,25 +67,38 @@ class SpotCollateralCalculator(CollateralCalculator):
perp_market: typing.Optional[GroupSlotPerpMarket] = slot.perp_market
if perp_market is None:
raise Exception(
f"Could not read spot or perp market of token {basket_token.base_instrument.symbol} at index {slot.index} of cache at {cache.address}")
f"Could not read spot or perp market of token {basket_token.base_instrument.symbol} at index {slot.index} of cache at {cache.address}"
)
init_asset_weight = perp_market.init_asset_weight
init_liab_weight = perp_market.init_liab_weight
in_orders: Decimal = Decimal(0)
if basket_token.spot_open_orders is not None and str(basket_token.spot_open_orders) in all_open_orders:
open_orders: OpenOrders = all_open_orders[str(basket_token.spot_open_orders)]
in_orders = open_orders.quote_token_total + \
(open_orders.base_token_total * token_price.value * init_asset_weight)
if (
basket_token.spot_open_orders is not None
and str(basket_token.spot_open_orders) in all_open_orders
):
open_orders: OpenOrders = all_open_orders[
str(basket_token.spot_open_orders)
]
in_orders = open_orders.quote_token_total + (
open_orders.base_token_total * token_price.value * init_asset_weight
)
# Base token calculations:
# total_collateral += prices[i] * (init_asset_weights[i] * deposits[i] * deposit_index - init_liab_weights[i] * borrows[i] * borrow_index)
# Note: the `AccountSlot` in the `Account` already factors the deposit and borrow index.
weighted: Decimal = in_orders + (token_price.value * ((
basket_token.deposit.value * init_asset_weight) - (
basket_token.borrow.value * init_liab_weight)))
weighted: Decimal = in_orders + (
token_price.value
* (
(basket_token.deposit.value * init_asset_weight)
- (basket_token.borrow.value * init_liab_weight)
)
)
if weighted != 0:
collateral_description += [f"{weighted:,.8f} USDC from {basket_token.base_instrument.symbol}"]
collateral_description += [
f"{weighted:,.8f} USDC from {basket_token.base_instrument.symbol}"
]
total += weighted
self._logger.debug(f"Weighted collateral: {', '.join(collateral_description)}")

View File

@ -33,8 +33,12 @@ class UnsettledFundingParams:
def calculate_unsettled_funding(params: UnsettledFundingParams) -> InstrumentValue:
result: Decimal
if params.base_position > 0:
result = params.base_position.value * (params.long_funding - params.long_settled_funding)
result = params.base_position.value * (
params.long_funding - params.long_settled_funding
)
else:
result = params.base_position.value * (params.short_funding - params.short_settled_funding)
result = params.base_position.value * (
params.short_funding - params.short_settled_funding
)
return InstrumentValue(params.quote_token, result)

View File

@ -34,7 +34,14 @@ from solana.publickey import PublicKey
from solana.rpc.api import Client
from solana.rpc.commitment import Commitment, Processed, Finalized
from solana.rpc.providers.http import HTTPProvider
from solana.rpc.types import DataSliceOpts, MemcmpOpts, RPCMethod, RPCResponse, TokenAccountOpts, TxOpts
from solana.rpc.types import (
DataSliceOpts,
MemcmpOpts,
RPCMethod,
RPCResponse,
TokenAccountOpts,
TxOpts,
)
from solana.transaction import Transaction
from .constants import SOL_DECIMAL_DIVISOR
@ -123,7 +130,12 @@ class TooManyRequestsRateLimitException(RateLimitException):
# considers it 'recent') or when it's too new (and hasn't yet made it to the node that is responding).
#
class BlockhashNotFoundException(ClientException):
def __init__(self, name: str, cluster_rpc_url: str, blockhash: typing.Optional[Blockhash] = None) -> None:
def __init__(
self,
name: str,
cluster_rpc_url: str,
blockhash: typing.Optional[Blockhash] = None,
) -> None:
message: str = f"Blockhash '{blockhash}' not found on {cluster_rpc_url}."
super().__init__(message, name, cluster_rpc_url)
self.blockhash: typing.Optional[Blockhash] = blockhash
@ -164,7 +176,13 @@ class TransactionAlreadyProcessedException(RateLimitException):
# A `StaleSlotException` exception allows trapping and handling exceptions when data is received from
#
class StaleSlotException(ClientException):
def __init__(self, name: str, cluster_rpc_url: str, latest_seen_slot: int, just_returned_slot: int) -> None:
def __init__(
self,
name: str,
cluster_rpc_url: str,
latest_seen_slot: int,
just_returned_slot: int,
) -> None:
message: str = f"Stale slot received - received data from slot {just_returned_slot} having previously seen slot {latest_seen_slot}."
super().__init__(message, name, cluster_rpc_url)
self.latest_seen_slot: int = latest_seen_slot
@ -180,7 +198,13 @@ class StaleSlotException(ClientException):
# to fetch a recent or distinct blockhash.
#
class FailedToFetchBlockhashException(ClientException):
def __init__(self, message: str, name: str, cluster_rpc_url: str, pauses: typing.Sequence[float]) -> None:
def __init__(
self,
message: str,
name: str,
cluster_rpc_url: str,
pauses: typing.Sequence[float],
) -> None:
super().__init__(message, name, cluster_rpc_url)
self.pauses: typing.Sequence[float] = pauses
@ -198,7 +222,21 @@ class FailedToFetchBlockhashException(ClientException):
# of problems at the right place.
#
class TransactionException(ClientException):
def __init__(self, transaction: typing.Optional[Transaction], message: str, code: int, name: str, cluster_rpc_url: str, rpc_method: str, request_text: str, response_text: str, accounts: typing.Union[str, typing.List[str], None], errors: typing.Union[str, typing.List[str], None], logs: typing.Union[str, typing.List[str], None], instruction_reporter: InstructionReporter = InstructionReporter()) -> None:
def __init__(
self,
transaction: typing.Optional[Transaction],
message: str,
code: int,
name: str,
cluster_rpc_url: str,
rpc_method: str,
request_text: str,
response_text: str,
accounts: typing.Union[str, typing.List[str], None],
errors: typing.Union[str, typing.List[str], None],
logs: typing.Union[str, typing.List[str], None],
instruction_reporter: InstructionReporter = InstructionReporter(),
) -> None:
super().__init__(message, name, cluster_rpc_url)
self.transaction: typing.Optional[Transaction] = transaction
self.code: int = code
@ -206,7 +244,9 @@ class TransactionException(ClientException):
self.request_text: str = request_text
self.response_text: str = response_text
def _ensure_list(item: typing.Union[str, typing.List[str], None]) -> typing.List[str]:
def _ensure_list(
item: typing.Union[str, typing.List[str], None]
) -> typing.List[str]:
if item is None:
return []
if isinstance(item, str):
@ -214,6 +254,7 @@ class TransactionException(ClientException):
if isinstance(item, list):
return item
return [f"{item}"]
self.accounts: typing.Sequence[str] = _ensure_list(accounts)
self.errors: typing.Sequence[str] = _ensure_list(errors)
self.logs: typing.Sequence[str] = expand_log_messages(_ensure_list(logs))
@ -224,17 +265,32 @@ class TransactionException(ClientException):
transaction_details = ""
if self.transaction is not None:
instruction_details = "\n".join(
list(map(self.instruction_reporter.report, self.transaction.instructions)))
transaction_details = "\n Instructions:\n " + instruction_details.replace("\n", "\n ")
list(
map(
self.instruction_reporter.report,
self.transaction.instructions,
)
)
)
transaction_details = (
"\n Instructions:\n "
+ instruction_details.replace("\n", "\n ")
)
accounts = "No Accounts"
if len(self.accounts) > 0:
accounts = "\n ".join([f"{item}".replace("\n", "\n ") for item in self.accounts])
accounts = "\n ".join(
[f"{item}".replace("\n", "\n ") for item in self.accounts]
)
errors = "No Errors"
if len(self.errors) > 0:
errors = "\n ".join([f"{item}".replace("\n", "\n ") for item in self.errors])
errors = "\n ".join(
[f"{item}".replace("\n", "\n ") for item in self.errors]
)
logs = "No Logs"
if len(self.logs) > 0:
logs = "\n ".join([f"{item}".replace("\n", "\n ") for item in self.logs])
logs = "\n ".join(
[f"{item}".replace("\n", "\n ") for item in self.logs]
)
return f"""« TransactionException in '{self.name}' [{self.rpc_method}]: {self.code}:: {self.message}{transaction_details}
Accounts:
{accounts}
@ -267,11 +323,15 @@ class SlotHolder:
def latest_slot(self) -> int:
return self.__latest_slot
def require_data_from_fresh_slot(self, latest_slot: typing.Optional[int] = None) -> None:
def require_data_from_fresh_slot(
self, latest_slot: typing.Optional[int] = None
) -> None:
latest: int = latest_slot or self.latest_slot
if latest >= self.latest_slot:
self.__latest_slot = latest + 1
self._logger.debug(f"Requiring data from slot {self.latest_slot} onwards now.")
self._logger.debug(
f"Requiring data from slot {self.latest_slot} onwards now."
)
def is_acceptable(self, slot_to_check: int) -> bool:
if slot_to_check < self.__latest_slot:
@ -279,7 +339,9 @@ class SlotHolder:
if slot_to_check > self.__latest_slot:
self.__latest_slot = slot_to_check
self._logger.debug(f"Only accepting data from slot {self.latest_slot} onwards now.")
self._logger.debug(
f"Only accepting data from slot {self.latest_slot} onwards now."
)
return True
@ -318,7 +380,13 @@ class NullTransactionStatusCollector(TransactionStatusCollector):
class TransactionWatcher:
def __init__(self, client: Client, slot_holder: SlotHolder, signature: str, collector: TransactionStatusCollector):
def __init__(
self,
client: Client,
slot_holder: SlotHolder,
signature: str,
collector: TransactionStatusCollector,
):
self._logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.client: Client = client
self.slot_holder: SlotHolder = slot_holder
@ -327,9 +395,47 @@ class TransactionWatcher:
def report_on_transaction(self) -> None:
started_at: datetime = datetime.now()
for pause in [0.1, 0.2, 0.3, 0.4, 0.5, 0.5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]:
for pause in [
0.1,
0.2,
0.3,
0.4,
0.5,
0.5,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
1,
]:
transaction_response = self.client.get_signature_statuses([self.signature])
if "result" in transaction_response and "value" in transaction_response["result"]:
if (
"result" in transaction_response
and "value" in transaction_response["result"]
):
[status] = transaction_response["result"]["value"]
if status is not None:
delta: timedelta = datetime.now() - started_at
@ -356,27 +462,47 @@ class TransactionWatcher:
err = status["err"]
if err is not None:
self._logger.warning(
f"Transaction {self.signature} failed after {time_taken:.2f} seconds with error {err}")
self.collector.add_transaction(TransactionStatus(
self.signature, TransactionOutcome.FAIL, err, started_at, delta))
f"Transaction {self.signature} failed after {time_taken:.2f} seconds with error {err}"
)
self.collector.add_transaction(
TransactionStatus(
self.signature,
TransactionOutcome.FAIL,
err,
started_at,
delta,
)
)
return
confirmation_status: str = status["confirmationStatus"]
slot: int = status["slot"]
self.slot_holder.require_data_from_fresh_slot(slot)
self.collector.add_transaction(TransactionStatus(
self.signature, TransactionOutcome.SUCCESS, None, started_at, delta))
self.collector.add_transaction(
TransactionStatus(
self.signature,
TransactionOutcome.SUCCESS,
None,
started_at,
delta,
)
)
self._logger.info(
f"Transaction {self.signature} reached confirmation status '{confirmation_status}' in slot {slot} after {time_taken:.2f} seconds")
f"Transaction {self.signature} reached confirmation status '{confirmation_status}' in slot {slot} after {time_taken:.2f} seconds"
)
return
time.sleep(pause)
delta = datetime.now() - started_at
time_wasted_looking: float = delta.seconds + delta.microseconds / 1000000
self.collector.add_transaction(TransactionStatus(
self.signature, TransactionOutcome.TIMEOUT, None, started_at, delta))
self.collector.add_transaction(
TransactionStatus(
self.signature, TransactionOutcome.TIMEOUT, None, started_at, delta
)
)
self._logger.warning(
f"Transaction {self.signature} disappeared despite spending {time_wasted_looking:.2f} seconds waiting for it")
f"Transaction {self.signature} disappeared despite spending {time_wasted_looking:.2f} seconds waiting for it"
)
# # 🥭 RPCCaller class
@ -384,18 +510,31 @@ class TransactionWatcher:
# A `RPCCaller` extends the HTTPProvider with better error handling.
#
class RPCCaller(HTTPProvider):
def __init__(self, name: str, cluster_rpc_url: str, cluster_ws_url: str, http_request_timeout: float, stale_data_pauses_before_retry: typing.Sequence[float], slot_holder: SlotHolder, instruction_reporter: InstructionReporter):
def __init__(
self,
name: str,
cluster_rpc_url: str,
cluster_ws_url: str,
http_request_timeout: float,
stale_data_pauses_before_retry: typing.Sequence[float],
slot_holder: SlotHolder,
instruction_reporter: InstructionReporter,
):
super().__init__(cluster_rpc_url)
self._logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.name: str = name
self.cluster_rpc_url: str = cluster_rpc_url
self.cluster_ws_url: str = cluster_ws_url
self.http_request_timeout: float = http_request_timeout
self.stale_data_pauses_before_retry: typing.Sequence[float] = stale_data_pauses_before_retry
self.stale_data_pauses_before_retry: typing.Sequence[
float
] = stale_data_pauses_before_retry
self.slot_holder: SlotHolder = slot_holder
self.instruction_reporter: InstructionReporter = instruction_reporter
def require_data_from_fresh_slot(self, latest_slot: typing.Optional[int] = None) -> None:
def require_data_from_fresh_slot(
self, latest_slot: typing.Optional[int] = None
) -> None:
self.slot_holder.require_data_from_fresh_slot(latest_slot)
def make_request(self, method: RPCMethod, *params: typing.Any) -> RPCResponse:
@ -425,7 +564,9 @@ class RPCCaller(HTTPProvider):
}
except StaleSlotException as exception:
last_stale_slot_exception = exception
self._logger.debug(f"Will retry after pause of {pause} seconds after getting stale slot: {exception}")
self._logger.debug(
f"Will retry after pause of {pause} seconds after getting stale slot: {exception}"
)
time.sleep(pause)
at_least_one_submission = True
@ -440,9 +581,12 @@ class RPCCaller(HTTPProvider):
# raw_response = requests.post(**request_kwargs)
# return self._after_request(raw_response=raw_response, method=method)
request_kwargs = self._before_request(method=method, params=params, is_async=False)
http_post_timeout: typing.Union[float,
None] = self.http_request_timeout if self.http_request_timeout >= 0 else None
request_kwargs = self._before_request(
method=method, params=params, is_async=False
)
http_post_timeout: typing.Union[float, None] = (
self.http_request_timeout if self.http_request_timeout >= 0 else None
)
raw_response = requests.post(**request_kwargs, timeout=http_post_timeout)
# Some custom exceptions specifically for rate-limiting. This allows calling code to handle this
@ -451,10 +595,16 @@ class RPCCaller(HTTPProvider):
# "You will see HTTP respose codes 429 for too many requests or 413 for too much bandwidth."
if raw_response.status_code == 413:
raise TooMuchBandwidthRateLimitException(
f"Rate limited (too much bandwidth) calling method '{method}' on {self.cluster_rpc_url}", self.name, self.cluster_rpc_url)
f"Rate limited (too much bandwidth) calling method '{method}' on {self.cluster_rpc_url}",
self.name,
self.cluster_rpc_url,
)
elif raw_response.status_code == 429:
raise TooManyRequestsRateLimitException(
f"Rate limited (too many requests) calling method '{method}' on {self.cluster_rpc_url}", self.name, self.cluster_rpc_url)
f"Rate limited (too many requests) calling method '{method}' on {self.cluster_rpc_url}",
self.name,
self.cluster_rpc_url,
)
# Not a rate-limit problem, but maybe there was some other error?
raw_response.raise_for_status()
@ -468,27 +618,60 @@ class RPCCaller(HTTPProvider):
# newer slot.
#
# Only do this check if we're using a commitment level of 'processed'.
if isinstance(params, Mapping) and len(params) > 1 and "commitment" in params[1] and params[1]["commitment"] == Processed:
if "result" in response and isinstance(response["result"], Mapping) and "context" in response["result"] and isinstance(response["result"]["context"], Mapping) and "slot" in response["result"]["context"]:
if (
isinstance(params, Mapping)
and len(params) > 1
and "commitment" in params[1]
and params[1]["commitment"] == Processed
):
if (
"result" in response
and isinstance(response["result"], Mapping)
and "context" in response["result"]
and isinstance(response["result"]["context"], Mapping)
and "slot" in response["result"]["context"]
):
slot: int = response["result"]["context"]["slot"]
if not self.slot_holder.is_acceptable(slot):
self._logger.warning(
f"Result is from stale slot: {slot} - latest slot is: {self.slot_holder.latest_slot}")
raise StaleSlotException(self.name, self.cluster_rpc_url, self.slot_holder.latest_slot, slot)
f"Result is from stale slot: {slot} - latest slot is: {self.slot_holder.latest_slot}"
)
raise StaleSlotException(
self.name,
self.cluster_rpc_url,
self.slot_holder.latest_slot,
slot,
)
if "error" in response:
if response["error"] is str:
message: str = typing.cast(str, response["error"])
raise ClientException(f"Transaction failed: '{message}'", self.name, self.cluster_rpc_url)
raise ClientException(
f"Transaction failed: '{message}'", self.name, self.cluster_rpc_url
)
else:
error = response["error"]
error_message: str = error["message"] if "message" in error else "No message"
error_data: typing.Dict[str, typing.Any] = error["data"] if "data" in error else {}
error_accounts = error_data["accounts"] if "accounts" in error_data else "No accounts"
error_message: str = (
error["message"] if "message" in error else "No message"
)
error_data: typing.Dict[str, typing.Any] = (
error["data"] if "data" in error else {}
)
error_accounts = (
error_data["accounts"]
if "accounts" in error_data
else "No accounts"
)
error_code: int = error["code"] if "code" in error else -1
error_err = error_data["err"] if "err" in error_data else "No error text returned"
error_err = (
error_data["err"]
if "err" in error_data
else "No error text returned"
)
error_logs = error_data["logs"] if "logs" in error_data else "No logs"
parameters = json.dumps({"jsonrpc": "2.0", "method": method, "params": params})
parameters = json.dumps(
{"jsonrpc": "2.0", "method": method, "params": params}
)
transaction: typing.Optional[Transaction] = None
blockhash: typing.Optional[Blockhash] = None
@ -497,25 +680,54 @@ class RPCCaller(HTTPProvider):
blockhash = transaction.recent_blockhash
if error_code == -32005:
slots_behind: int = error["data"]["numSlotsBehind"] if "numSlotsBehind" in error["data"] else -1
raise NodeIsBehindException(self.name, self.cluster_rpc_url, slots_behind)
slots_behind: int = (
error["data"]["numSlotsBehind"]
if "numSlotsBehind" in error["data"]
else -1
)
raise NodeIsBehindException(
self.name, self.cluster_rpc_url, slots_behind
)
if error_err == "BlockhashNotFound":
raise BlockhashNotFoundException(self.name, self.cluster_rpc_url, blockhash)
raise BlockhashNotFoundException(
self.name, self.cluster_rpc_url, blockhash
)
if error_err == "AlreadyProcessed":
raise TransactionAlreadyProcessedException(error_message, self.name, self.cluster_rpc_url)
raise TransactionAlreadyProcessedException(
error_message, self.name, self.cluster_rpc_url
)
exception_message: str = f"Transaction failed with: '{error_message}'"
raise TransactionException(transaction, exception_message, error_code, self.name,
self.cluster_rpc_url, method, parameters, response_text, error_accounts,
error_err, error_logs, self.instruction_reporter)
raise TransactionException(
transaction,
exception_message,
error_code,
self.name,
self.cluster_rpc_url,
method,
parameters,
response_text,
error_accounts,
error_err,
error_logs,
self.instruction_reporter,
)
if method == "getRecentBlockhash":
if "result" in response and "value" in response["result"] and "blockhash" in response["result"]["value"] and "context" in response["result"] and "slot" in response["result"]["context"]:
if (
"result" in response
and "value" in response["result"]
and "blockhash" in response["result"]["value"]
and "context" in response["result"]
and "slot" in response["result"]["context"]
):
fresh_blockhash = Blockhash(response["result"]["value"]["blockhash"])
fresh_blockhash_slot = Blockhash(response["result"]["context"]["slot"])
self._logger.debug(f"Recent blockhash [slot: {fresh_blockhash_slot}]: {fresh_blockhash}")
self._logger.debug(
f"Recent blockhash [slot: {fresh_blockhash_slot}]: {fresh_blockhash}"
)
# The call succeeded.
return typing.cast(RPCResponse, response)
@ -569,19 +781,28 @@ class CompoundRPCCaller(HTTPProvider):
successful_index: int = self.__providers.index(provider)
if successful_index != 0:
# Rebase the providers' list so we continue to use this successful one (until it fails)
self.__providers = [*self.__providers[successful_index:], *self.__providers[:successful_index]]
self.__providers = [
*self.__providers[successful_index:],
*self.__providers[:successful_index],
]
self.on_provider_change()
self._logger.debug(f"Shifted provider - now using: {self.__providers[0]}")
self._logger.debug(
f"Shifted provider - now using: {self.__providers[0]}"
)
return result
except (requests.exceptions.HTTPError,
requests.exceptions.ConnectionError,
requests.exceptions.Timeout,
RateLimitException,
NodeIsBehindException,
StaleSlotException,
FailedToFetchBlockhashException) as exception:
except (
requests.exceptions.HTTPError,
requests.exceptions.ConnectionError,
requests.exceptions.Timeout,
RateLimitException,
NodeIsBehindException,
StaleSlotException,
FailedToFetchBlockhashException,
) as exception:
all_exceptions += [exception]
self._logger.info(f"Moving to next provider - {provider} gave {exception}")
self._logger.info(
f"Moving to next provider - {provider} gave {exception}"
)
if len(all_exceptions) == 1:
raise all_exceptions[0]
@ -616,7 +837,18 @@ class ClusterUrlData:
class BetterClient:
def __init__(self, client: Client, name: str, cluster_name: str, commitment: Commitment, skip_preflight: bool, encoding: str, blockhash_cache_duration: int, rpc_caller: CompoundRPCCaller, transaction_status_collector: TransactionStatusCollector = NullTransactionStatusCollector()) -> None:
def __init__(
self,
client: Client,
name: str,
cluster_name: str,
commitment: Commitment,
skip_preflight: bool,
encoding: str,
blockhash_cache_duration: int,
rpc_caller: CompoundRPCCaller,
transaction_status_collector: TransactionStatusCollector = NullTransactionStatusCollector(),
) -> None:
self._logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.compatible_client: Client = client
self.name: str = name
@ -627,23 +859,48 @@ class BetterClient:
self.blockhash_cache_duration: int = blockhash_cache_duration
self.rpc_caller: CompoundRPCCaller = rpc_caller
self.executor: Executor = ThreadPoolExecutor()
self.transaction_status_collector: TransactionStatusCollector = transaction_status_collector
self.transaction_status_collector: TransactionStatusCollector = (
transaction_status_collector
)
@staticmethod
def from_configuration(name: str, cluster_name: str, cluster_urls: typing.Sequence[ClusterUrlData], commitment: Commitment, skip_preflight: bool, encoding: str, blockhash_cache_duration: int, http_request_timeout: float, stale_data_pauses_before_retry: typing.Sequence[float], instruction_reporter: InstructionReporter, transaction_status_collector: TransactionStatusCollector) -> "BetterClient":
def from_configuration(
name: str,
cluster_name: str,
cluster_urls: typing.Sequence[ClusterUrlData],
commitment: Commitment,
skip_preflight: bool,
encoding: str,
blockhash_cache_duration: int,
http_request_timeout: float,
stale_data_pauses_before_retry: typing.Sequence[float],
instruction_reporter: InstructionReporter,
transaction_status_collector: TransactionStatusCollector,
) -> "BetterClient":
slot_holder: SlotHolder = SlotHolder()
rpc_callers: typing.List[RPCCaller] = []
cluster_url: ClusterUrlData
for cluster_url in cluster_urls:
rpc_caller: RPCCaller = RPCCaller(name, cluster_url.rpc, cluster_url.ws, http_request_timeout, stale_data_pauses_before_retry,
slot_holder, instruction_reporter)
rpc_caller: RPCCaller = RPCCaller(
name,
cluster_url.rpc,
cluster_url.ws,
http_request_timeout,
stale_data_pauses_before_retry,
slot_holder,
instruction_reporter,
)
rpc_callers += [rpc_caller]
provider: CompoundRPCCaller = CompoundRPCCaller(name, rpc_callers)
blockhash_cache: typing.Union[BlockhashCache, bool] = False
if blockhash_cache_duration > 0:
blockhash_cache = BlockhashCache(blockhash_cache_duration)
client: Client = Client(endpoint=cluster_url.rpc, commitment=commitment, blockhash_cache=blockhash_cache)
client: Client = Client(
endpoint=cluster_url.rpc,
commitment=commitment,
blockhash_cache=blockhash_cache,
)
client._provider = provider
def __on_provider_change() -> None:
@ -656,7 +913,17 @@ class BetterClient:
provider.on_provider_change = __on_provider_change
return BetterClient(client, name, cluster_name, commitment, skip_preflight, encoding, blockhash_cache_duration, provider, transaction_status_collector)
return BetterClient(
client,
name,
cluster_name,
commitment,
skip_preflight,
encoding,
blockhash_cache_duration,
provider,
transaction_status_collector,
)
@property
def cluster_rpc_url(self) -> str:
@ -664,7 +931,9 @@ class BetterClient:
@property
def cluster_rpc_urls(self) -> typing.Sequence[str]:
return [rpc_caller.cluster_rpc_url for rpc_caller in self.rpc_caller.all_providers]
return [
rpc_caller.cluster_rpc_url for rpc_caller in self.rpc_caller.all_providers
]
@property
def cluster_ws_url(self) -> str:
@ -672,7 +941,9 @@ class BetterClient:
@property
def cluster_ws_urls(self) -> typing.Sequence[str]:
return [rpc_caller.cluster_ws_url for rpc_caller in self.rpc_caller.all_providers]
return [
rpc_caller.cluster_ws_url for rpc_caller in self.rpc_caller.all_providers
]
@property
def cluster_urls(self) -> typing.Sequence[ClusterUrlData]:
@ -692,69 +963,137 @@ class BetterClient:
def require_data_from_fresh_slot(self) -> None:
self.rpc_caller.current.require_data_from_fresh_slot()
def get_balance(self, pubkey: typing.Union[PublicKey, str], commitment: Commitment = UnspecifiedCommitment) -> Decimal:
def get_balance(
self,
pubkey: typing.Union[PublicKey, str],
commitment: Commitment = UnspecifiedCommitment,
) -> Decimal:
resolved_commitment, _ = self.__resolve_defaults(commitment)
response = self.compatible_client.get_balance(pubkey, resolved_commitment)
value = Decimal(response["result"]["value"])
return value / SOL_DECIMAL_DIVISOR
def get_account_info(self, pubkey: typing.Union[PublicKey, str], commitment: Commitment = UnspecifiedCommitment,
encoding: str = UnspecifiedEncoding, data_slice: typing.Optional[DataSliceOpts] = None) -> typing.Any:
resolved_commitment, resolved_encoding = self.__resolve_defaults(commitment, encoding)
response = self.compatible_client.get_account_info(pubkey, resolved_commitment, resolved_encoding, data_slice)
def get_account_info(
self,
pubkey: typing.Union[PublicKey, str],
commitment: Commitment = UnspecifiedCommitment,
encoding: str = UnspecifiedEncoding,
data_slice: typing.Optional[DataSliceOpts] = None,
) -> typing.Any:
resolved_commitment, resolved_encoding = self.__resolve_defaults(
commitment, encoding
)
response = self.compatible_client.get_account_info(
pubkey, resolved_commitment, resolved_encoding, data_slice
)
return response["result"]
def get_confirmed_signatures_for_address2(self, account: typing.Union[str, Keypair, PublicKey], before: typing.Optional[str] = None, until: typing.Optional[str] = None, limit: typing.Optional[int] = None) -> typing.Sequence[str]:
response = self.compatible_client.get_confirmed_signature_for_address2(account, before, until, limit)
def get_confirmed_signatures_for_address2(
self,
account: typing.Union[str, Keypair, PublicKey],
before: typing.Optional[str] = None,
until: typing.Optional[str] = None,
limit: typing.Optional[int] = None,
) -> typing.Sequence[str]:
response = self.compatible_client.get_confirmed_signature_for_address2(
account, before, until, limit
)
return [result["signature"] for result in response["result"]]
def get_confirmed_transaction(self, signature: str, encoding: str = "json") -> typing.Any:
def get_confirmed_transaction(
self, signature: str, encoding: str = "json"
) -> typing.Any:
_, resolved_encoding = self.__resolve_defaults(None, encoding)
response = self.compatible_client.get_confirmed_transaction(signature, resolved_encoding)
response = self.compatible_client.get_confirmed_transaction(
signature, resolved_encoding
)
return response["result"]
def get_minimum_balance_for_rent_exemption(self, size: int, commitment: Commitment = UnspecifiedCommitment) -> int:
def get_minimum_balance_for_rent_exemption(
self, size: int, commitment: Commitment = UnspecifiedCommitment
) -> int:
resolved_commitment, _ = self.__resolve_defaults(commitment)
response = self.compatible_client.get_minimum_balance_for_rent_exemption(size, resolved_commitment)
response = self.compatible_client.get_minimum_balance_for_rent_exemption(
size, resolved_commitment
)
return int(response["result"])
def get_program_accounts(self, pubkey: typing.Union[str, PublicKey],
commitment: Commitment = UnspecifiedCommitment,
encoding: typing.Optional[str] = UnspecifiedEncoding,
data_slice: typing.Optional[DataSliceOpts] = None,
data_size: typing.Optional[int] = None,
memcmp_opts: typing.Optional[typing.List[MemcmpOpts]] = None) -> typing.Any:
resolved_commitment, resolved_encoding = self.__resolve_defaults(commitment, encoding)
def get_program_accounts(
self,
pubkey: typing.Union[str, PublicKey],
commitment: Commitment = UnspecifiedCommitment,
encoding: typing.Optional[str] = UnspecifiedEncoding,
data_slice: typing.Optional[DataSliceOpts] = None,
data_size: typing.Optional[int] = None,
memcmp_opts: typing.Optional[typing.List[MemcmpOpts]] = None,
) -> typing.Any:
resolved_commitment, resolved_encoding = self.__resolve_defaults(
commitment, encoding
)
response = self.compatible_client.get_program_accounts(
pubkey, resolved_commitment, resolved_encoding, data_slice, data_size, memcmp_opts)
pubkey,
resolved_commitment,
resolved_encoding,
data_slice,
data_size,
memcmp_opts,
)
return response["result"]
def get_recent_blockhash(self, commitment: Commitment = UnspecifiedCommitment) -> Blockhash:
def get_recent_blockhash(
self, commitment: Commitment = UnspecifiedCommitment
) -> Blockhash:
resolved_commitment, _ = self.__resolve_defaults(commitment)
response = self.compatible_client.get_recent_blockhash(resolved_commitment)
return Blockhash(response["result"]["value"]["blockhash"])
def get_token_account_balance(self, pubkey: typing.Union[str, PublicKey], commitment: Commitment = UnspecifiedCommitment) -> Decimal:
def get_token_account_balance(
self,
pubkey: typing.Union[str, PublicKey],
commitment: Commitment = UnspecifiedCommitment,
) -> Decimal:
resolved_commitment, _ = self.__resolve_defaults(commitment)
response = self.compatible_client.get_token_account_balance(pubkey, resolved_commitment)
response = self.compatible_client.get_token_account_balance(
pubkey, resolved_commitment
)
value = Decimal(response["result"]["value"]["amount"])
decimal_places = response["result"]["value"]["decimals"]
divisor = Decimal(10 ** decimal_places)
divisor = Decimal(10**decimal_places)
return value / divisor
def get_token_accounts_by_owner(self, owner: PublicKey, token_account_options: TokenAccountOpts, commitment: Commitment = UnspecifiedCommitment,) -> typing.Any:
def get_token_accounts_by_owner(
self,
owner: PublicKey,
token_account_options: TokenAccountOpts,
commitment: Commitment = UnspecifiedCommitment,
) -> typing.Any:
resolved_commitment, _ = self.__resolve_defaults(commitment)
response = self.compatible_client.get_token_accounts_by_owner(owner, token_account_options, resolved_commitment)
response = self.compatible_client.get_token_accounts_by_owner(
owner, token_account_options, resolved_commitment
)
return response["result"]["value"]
def get_multiple_accounts(self, pubkeys: typing.List[typing.Union[PublicKey, str]], commitment: Commitment = UnspecifiedCommitment,
encoding: str = UnspecifiedEncoding, data_slice: typing.Optional[DataSliceOpts] = None) -> typing.Any:
resolved_commitment, resolved_encoding = self.__resolve_defaults(commitment, encoding)
def get_multiple_accounts(
self,
pubkeys: typing.List[typing.Union[PublicKey, str]],
commitment: Commitment = UnspecifiedCommitment,
encoding: str = UnspecifiedEncoding,
data_slice: typing.Optional[DataSliceOpts] = None,
) -> typing.Any:
resolved_commitment, resolved_encoding = self.__resolve_defaults(
commitment, encoding
)
response = self.compatible_client.get_multiple_accounts(
pubkeys, resolved_commitment, resolved_encoding, data_slice)
pubkeys, resolved_commitment, resolved_encoding, data_slice
)
return response["result"]["value"]
def send_transaction(self, transaction: Transaction, *signers: Keypair, opts: TxOpts = TxOpts(preflight_commitment=UnspecifiedCommitment)) -> str:
def send_transaction(
self,
transaction: Transaction,
*signers: Keypair,
opts: TxOpts = TxOpts(preflight_commitment=UnspecifiedCommitment),
) -> str:
# This method is an exception to the normal exception-handling to fail over to the next RPC provider.
#
# Normal RPC exceptions just move on to the next RPC provider and try again. That won't work with the
@ -773,17 +1112,25 @@ class BetterClient:
proper_commitment = self.commitment
proper_skip_preflight = self.skip_preflight
proper_opts = TxOpts(preflight_commitment=proper_commitment,
skip_confirmation=opts.skip_confirmation,
skip_preflight=proper_skip_preflight)
proper_opts = TxOpts(
preflight_commitment=proper_commitment,
skip_confirmation=opts.skip_confirmation,
skip_preflight=proper_skip_preflight,
)
response = self.compatible_client.send_transaction(transaction, *signers, opts=proper_opts)
response = self.compatible_client.send_transaction(
transaction, *signers, opts=proper_opts
)
signature: str = str(response["result"])
self._logger.debug(f"Transaction signature: {signature}")
if signature != _STUB_TRANSACTION_SIGNATURE:
tx_reporter: TransactionWatcher = TransactionWatcher(
self.compatible_client, self.rpc_caller.current.slot_holder, signature, self.transaction_status_collector)
self.compatible_client,
self.rpc_caller.current.slot_holder,
signature,
self.transaction_status_collector,
)
self.executor.submit(tx_reporter.report_on_transaction)
else:
self._logger.error("Could not get status for stub signature")
@ -791,15 +1138,20 @@ class BetterClient:
return signature
except BlockhashNotFoundException as blockhash_not_found_exception:
self._logger.debug(
f"Trying next provider after intercepting blockhash exception on provider {provider}: {blockhash_not_found_exception}")
f"Trying next provider after intercepting blockhash exception on provider {provider}: {blockhash_not_found_exception}"
)
last_exception = blockhash_not_found_exception
transaction.recent_blockhash = None
self.rpc_caller.shift_to_next_provider()
raise last_exception
def wait_for_confirmation(self, transaction_ids: typing.Sequence[str], max_wait_in_seconds: int = 60) -> typing.Sequence[str]:
self._logger.info(f"Waiting up to {max_wait_in_seconds} seconds for {transaction_ids}.")
def wait_for_confirmation(
self, transaction_ids: typing.Sequence[str], max_wait_in_seconds: int = 60
) -> typing.Sequence[str]:
self._logger.info(
f"Waiting up to {max_wait_in_seconds} seconds for {transaction_ids}."
)
all_confirmed: typing.List[str] = []
start_time: datetime = datetime.now()
cutoff: datetime = start_time + timedelta(seconds=max_wait_in_seconds)
@ -809,15 +1161,22 @@ class BetterClient:
confirmed = self.get_confirmed_transaction(transaction_id)
if confirmed is not None:
self._logger.info(
f"Confirmed {transaction_id} after {datetime.now() - start_time} seconds.")
f"Confirmed {transaction_id} after {datetime.now() - start_time} seconds."
)
all_confirmed += [transaction_id]
break
if len(all_confirmed) != len(transaction_ids):
self._logger.info(f"Timed out after {max_wait_in_seconds} seconds waiting on transaction {transaction_id}.")
self._logger.info(
f"Timed out after {max_wait_in_seconds} seconds waiting on transaction {transaction_id}."
)
return all_confirmed
def __resolve_defaults(self, commitment: typing.Optional[Commitment], encoding: typing.Optional[str] = None) -> typing.Tuple[Commitment, str]:
def __resolve_defaults(
self,
commitment: typing.Optional[Commitment],
encoding: typing.Optional[str] = None,
) -> typing.Tuple[Commitment, str]:
if commitment is None or commitment == UnspecifiedCommitment:
commitment = self.commitment

View File

@ -31,18 +31,27 @@ _PUBKEY_LENGTH = 32
_SIGNATURE_LENGTH = 64
def _split_instructions_into_chunks(context: Context, signers: typing.Sequence[Keypair], instructions: typing.Sequence[TransactionInstruction]) -> typing.Sequence[typing.Sequence[TransactionInstruction]]:
def _split_instructions_into_chunks(
context: Context,
signers: typing.Sequence[Keypair],
instructions: typing.Sequence[TransactionInstruction],
) -> typing.Sequence[typing.Sequence[TransactionInstruction]]:
vetted_chunks: typing.List[typing.List[TransactionInstruction]] = []
current_chunk: typing.List[TransactionInstruction] = []
for counter, instruction in enumerate(instructions):
instruction_size_on_its_own = CombinableInstructions.transaction_size(signers, [instruction])
instruction_size_on_its_own = CombinableInstructions.transaction_size(
signers, [instruction]
)
if instruction_size_on_its_own >= _MAXIMUM_TRANSACTION_LENGTH:
report = context.client.instruction_reporter.report(instruction)
raise Exception(
f"Instruction exceeds maximum size - instruction {counter} has {len(instruction.keys)} keys and creates a transaction {instruction_size_on_its_own} bytes long:\n{report}")
f"Instruction exceeds maximum size - instruction {counter} has {len(instruction.keys)} keys and creates a transaction {instruction_size_on_its_own} bytes long:\n{report}"
)
in_progress_chunk = current_chunk + [instruction]
transaction_size = CombinableInstructions.transaction_size(signers, in_progress_chunk)
transaction_size = CombinableInstructions.transaction_size(
signers, in_progress_chunk
)
if transaction_size < _MAXIMUM_TRANSACTION_LENGTH:
current_chunk = in_progress_chunk
else:
@ -54,7 +63,8 @@ def _split_instructions_into_chunks(context: Context, signers: typing.Sequence[K
total_in_chunks = sum(map(lambda chunk: len(chunk), all_chunks))
if total_in_chunks != len(instructions):
raise Exception(
f"Failed to chunk instructions. Have {total_in_chunks} instuctions in chunks. Should have {len(instructions)}.")
f"Failed to chunk instructions. Have {total_in_chunks} instuctions in chunks. Should have {len(instructions)}."
)
return all_chunks
@ -69,11 +79,15 @@ def _split_instructions_into_chunks(context: Context, signers: typing.Sequence[K
# (signers + place_orders + settle + crank).execute(context)
# ```
#
class CombinableInstructions():
class CombinableInstructions:
# A toggle to run both checks to ensure our calculations are accurate.
__check_transaction_size_with_pyserum = False
def __init__(self, signers: typing.Sequence[Keypair], instructions: typing.Sequence[TransactionInstruction]) -> None:
def __init__(
self,
signers: typing.Sequence[Keypair],
instructions: typing.Sequence[TransactionInstruction],
) -> None:
self._logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.signers: typing.Sequence[Keypair] = signers
self.instructions: typing.Sequence[TransactionInstruction] = instructions
@ -91,12 +105,17 @@ class CombinableInstructions():
return CombinableInstructions(signers=[wallet.keypair], instructions=[])
@staticmethod
def from_instruction(instruction: TransactionInstruction) -> "CombinableInstructions":
def from_instruction(
instruction: TransactionInstruction,
) -> "CombinableInstructions":
return CombinableInstructions(signers=[], instructions=[instruction])
# This is the expensive - but always accurate - way of calculating the size of a transaction.
@staticmethod
def _transaction_size_from_pyserum(signers: typing.Sequence[Keypair], instructions: typing.Sequence[TransactionInstruction]) -> int:
def _transaction_size_from_pyserum(
signers: typing.Sequence[Keypair],
instructions: typing.Sequence[TransactionInstruction],
) -> int:
inspector = Transaction()
inspector.recent_blockhash = Blockhash(str(PublicKey(3)))
inspector.instructions.extend(instructions)
@ -109,14 +128,17 @@ class CombinableInstructions():
length += 1
# Signatures
length += (len(inspector.signatures) * _SIGNATURE_LENGTH)
length += len(inspector.signatures) * _SIGNATURE_LENGTH
return length
# This is the quicker way - just add up the sizes ourselves. It's not trivial though.
@staticmethod
def _calculate_transaction_size(signers: typing.Sequence[Keypair], instructions: typing.Sequence[TransactionInstruction]) -> int:
def _calculate_transaction_size(
signers: typing.Sequence[Keypair],
instructions: typing.Sequence[TransactionInstruction],
) -> int:
# Solana transactions have a deterministic size, but calculating it is a bit tricky.
#
# The transaction consists of:
@ -168,56 +190,94 @@ class CombinableInstructions():
def shortvec_length(value: int) -> int:
return len(shortvec.encode_length(value))
program_ids = {instruction.program_id.to_base58() for instruction in instructions}
meta_pubkeys = {meta.pubkey.to_base58() for instruction in instructions for meta in instruction.keys}
distinct_publickeys = set.union(program_ids, meta_pubkeys, {
signer.public_key.to_base58() for signer in signers})
program_ids = {
instruction.program_id.to_base58() for instruction in instructions
}
meta_pubkeys = {
meta.pubkey.to_base58()
for instruction in instructions
for meta in instruction.keys
}
distinct_publickeys = set.union(
program_ids,
meta_pubkeys,
{signer.public_key.to_base58() for signer in signers},
)
num_distinct_publickeys = len(distinct_publickeys)
# 35 + (shortvec-length of distinct public keys) + (32 * number of distinct public keys)
header_size = 35 + shortvec_length(num_distinct_publickeys) + (num_distinct_publickeys * _PUBKEY_LENGTH)
header_size = (
35
+ shortvec_length(num_distinct_publickeys)
+ (num_distinct_publickeys * _PUBKEY_LENGTH)
)
instruction_count_length = shortvec_length(len(instructions))
instructions_size = 0
for inst in instructions:
# 1 + (shortvec-length of number of keys) + (number of keys) + (shortvec-length of the data) + (length of the data)
instructions_size += 1 + shortvec_length(len(inst.keys)) + len(inst.keys) + \
shortvec_length(len(inst.data)) + len(inst.data)
instructions_size += (
1
+ shortvec_length(len(inst.keys))
+ len(inst.keys)
+ shortvec_length(len(inst.data))
+ len(inst.data)
)
# Signatures
signatures_size = 1 + (len(signers) * _SIGNATURE_LENGTH)
# We can now calculate the total transaction size
calculated_transaction_size = header_size + instruction_count_length + instructions_size + signatures_size
calculated_transaction_size = (
header_size + instruction_count_length + instructions_size + signatures_size
)
return calculated_transaction_size
# Calculate the exact size of a transaction. There's an upper limit of 1232 so we need to keep
# all transactions below this size.
@staticmethod
def transaction_size(signers: typing.Sequence[Keypair], instructions: typing.Sequence[TransactionInstruction]) -> int:
calculated_transaction_size = CombinableInstructions._calculate_transaction_size(signers, instructions)
def transaction_size(
signers: typing.Sequence[Keypair],
instructions: typing.Sequence[TransactionInstruction],
) -> int:
calculated_transaction_size = (
CombinableInstructions._calculate_transaction_size(signers, instructions)
)
if CombinableInstructions.__check_transaction_size_with_pyserum:
pyserum_transaction_size = CombinableInstructions._transaction_size_from_pyserum(signers, instructions)
pyserum_transaction_size = (
CombinableInstructions._transaction_size_from_pyserum(
signers, instructions
)
)
discrepancy = pyserum_transaction_size - calculated_transaction_size
if discrepancy == 0:
logging.debug(
f"txszcalc Calculated: {calculated_transaction_size}, Should be: {pyserum_transaction_size}, No Discrepancy!")
f"txszcalc Calculated: {calculated_transaction_size}, Should be: {pyserum_transaction_size}, No Discrepancy!"
)
else:
logging.error(
f"txszcalcerr Calculated: {calculated_transaction_size}, Should be: {pyserum_transaction_size}, Discrepancy: {discrepancy}")
f"txszcalcerr Calculated: {calculated_transaction_size}, Should be: {pyserum_transaction_size}, Discrepancy: {discrepancy}"
)
return pyserum_transaction_size
return calculated_transaction_size
def __add__(self, new_instruction_data: "CombinableInstructions") -> "CombinableInstructions":
def __add__(
self, new_instruction_data: "CombinableInstructions"
) -> "CombinableInstructions":
all_signers = [*self.signers, *new_instruction_data.signers]
all_instructions = [*self.instructions, *new_instruction_data.instructions]
return CombinableInstructions(signers=all_signers, instructions=all_instructions)
return CombinableInstructions(
signers=all_signers, instructions=all_instructions
)
def execute(self, context: Context, on_exception_continue: bool = False) -> typing.Sequence[str]:
chunks: typing.Sequence[typing.Sequence[TransactionInstruction]
] = _split_instructions_into_chunks(context, self.signers, self.instructions)
def execute(
self, context: Context, on_exception_continue: bool = False
) -> typing.Sequence[str]:
chunks: typing.Sequence[
typing.Sequence[TransactionInstruction]
] = _split_instructions_into_chunks(context, self.signers, self.instructions)
if len(chunks) == 1 and len(chunks[0]) == 0:
self._logger.info("No instructions to run.")
@ -236,14 +296,18 @@ class CombinableInstructions():
except Exception as exception:
starts_at = sum(len(ch) for ch in chunks[0:index])
if on_exception_continue:
self._logger.error(f"""[{context.name}] Error executing chunk {index} (instructions {starts_at} to {starts_at + len(chunk)}) of CombinableInstruction.
{exception}""")
self._logger.error(
f"""[{context.name}] Error executing chunk {index} (instructions {starts_at} to {starts_at + len(chunk)}) of CombinableInstruction.
{exception}"""
)
else:
raise exception
return results
async def execute_async(self, context: Context, on_exception_continue: bool = False) -> typing.Sequence[str]:
async def execute_async(
self, context: Context, on_exception_continue: bool = False
) -> typing.Sequence[str]:
return self.execute(context, on_exception_continue)
def __str__(self) -> str:

View File

@ -54,7 +54,7 @@ SOL_DECIMALS = decimal.Decimal(9)
#
# The divisor to use to turn an integer value of SOLs from an account's `balance` into a value with the correct number of decimal places.
#
SOL_DECIMAL_DIVISOR = decimal.Decimal(10 ** SOL_DECIMALS)
SOL_DECIMAL_DIVISOR = decimal.Decimal(10**SOL_DECIMALS)
# ## NUM_TOKENS
@ -100,21 +100,31 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
# This function provides a consistent way to determine the correct data path for use throughout `mango-explorer`.
#
def _build_data_path() -> str:
possibilities: typing.Sequence[str] = ["../data", "data", ".", "../../data", "../../../data"]
possibilities: typing.Sequence[str] = [
"../data",
"data",
".",
"../../data",
"../../../data",
]
attempts: typing.List[str] = []
file_root: str = os.path.dirname(__file__)
for possibility in possibilities:
data_path: str = os.path.normpath(os.path.join(file_root, possibility))
attempts += [data_path]
try:
attempted_ids_path: str = os.path.normpath(os.path.join(data_path, "ids.json"))
attempted_ids_path: str = os.path.normpath(
os.path.join(data_path, "ids.json")
)
with open(attempted_ids_path) as ids_file:
json.load(ids_file)
return data_path
except:
pass
raise Exception(f"Could not determine data path - ids.json not found in: {attempts}")
raise Exception(
f"Could not determine data path - ids.json not found in: {attempts}"
)
# # DATA_PATH

View File

@ -24,7 +24,12 @@ from rx.scheduler.threadpoolscheduler import ThreadPoolScheduler
from solana.publickey import PublicKey
from solana.rpc.commitment import Commitment
from .client import BetterClient, ClusterUrlData, TransactionStatusCollector, NullTransactionStatusCollector
from .client import (
BetterClient,
ClusterUrlData,
TransactionStatusCollector,
NullTransactionStatusCollector,
)
from .constants import MangoConstants
from .instructionreporter import InstructionReporter, CompoundInstructionReporter
from .instrumentlookup import InstrumentLookup
@ -37,19 +42,48 @@ from .text import indent_collection_as_str, indent_item_by
# A `Context` object to manage Solana connection and Mango configuration.
#
class Context:
def __init__(self, name: str, cluster_name: str, cluster_urls: typing.Sequence[ClusterUrlData], skip_preflight: bool,
commitment: str, encoding: str, blockhash_cache_duration: int, http_request_timeout: float,
stale_data_pauses_before_retry: typing.Sequence[float], mango_program_address: PublicKey,
serum_program_address: PublicKey, group_name: str, group_address: PublicKey,
gma_chunk_size: Decimal, gma_chunk_pause: Decimal, reflink: typing.Optional[PublicKey],
instrument_lookup: InstrumentLookup, market_lookup: MarketLookup,
transaction_status_collector: TransactionStatusCollector = NullTransactionStatusCollector()) -> None:
def __init__(
self,
name: str,
cluster_name: str,
cluster_urls: typing.Sequence[ClusterUrlData],
skip_preflight: bool,
commitment: str,
encoding: str,
blockhash_cache_duration: int,
http_request_timeout: float,
stale_data_pauses_before_retry: typing.Sequence[float],
mango_program_address: PublicKey,
serum_program_address: PublicKey,
group_name: str,
group_address: PublicKey,
gma_chunk_size: Decimal,
gma_chunk_pause: Decimal,
reflink: typing.Optional[PublicKey],
instrument_lookup: InstrumentLookup,
market_lookup: MarketLookup,
transaction_status_collector: TransactionStatusCollector = NullTransactionStatusCollector(),
) -> None:
self._logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.name: str = name
instruction_reporter: InstructionReporter = CompoundInstructionReporter.from_addresses(
mango_program_address, serum_program_address)
self.client: BetterClient = BetterClient.from_configuration(name, cluster_name, cluster_urls, Commitment(
commitment), skip_preflight, encoding, blockhash_cache_duration, http_request_timeout, stale_data_pauses_before_retry, instruction_reporter, transaction_status_collector)
instruction_reporter: InstructionReporter = (
CompoundInstructionReporter.from_addresses(
mango_program_address, serum_program_address
)
)
self.client: BetterClient = BetterClient.from_configuration(
name,
cluster_name,
cluster_urls,
Commitment(commitment),
skip_preflight,
encoding,
blockhash_cache_duration,
http_request_timeout,
stale_data_pauses_before_retry,
instruction_reporter,
transaction_status_collector,
)
self.mango_program_address: PublicKey = mango_program_address
self.serum_program_address: PublicKey = serum_program_address
self.group_name: str = group_name
@ -66,8 +100,13 @@ class Context:
# kangda said in Discord: https://discord.com/channels/791995070613159966/836239696467591186/847816026245693451
# "I think you are better off doing 4,8,16,20,30"
self.retry_pauses: typing.Sequence[Decimal] = [Decimal(4), Decimal(
8), Decimal(16), Decimal(20), Decimal(30)]
self.retry_pauses: typing.Sequence[Decimal] = [
Decimal(4),
Decimal(8),
Decimal(16),
Decimal(20),
Decimal(30),
]
def create_thread_pool_scheduler(self) -> ThreadPoolScheduler:
return ThreadPoolScheduler(multiprocessing.cpu_count())
@ -100,7 +139,10 @@ class Context:
def lookup_group_name(self, group_address: PublicKey) -> str:
group_address_str = str(group_address)
for group in MangoConstants["groups"]:
if group["cluster"] == self.client.cluster_name and group["publicKey"] == group_address_str:
if (
group["cluster"] == self.client.cluster_name
and group["publicKey"] == group_address_str
):
return str(group["name"])
return "« Unknown Group »"
@ -111,7 +153,9 @@ class Context:
return typing.cast(typing.Sequence[typing.Any], stats_response.json())
def __str__(self) -> str:
cluster_urls: str = indent_item_by(indent_collection_as_str(self.client.cluster_urls))
cluster_urls: str = indent_item_by(
indent_collection_as_str(self.client.cluster_urls)
)
return f"""« Context '{self.name}':
Cluster Name: {self.client.cluster_name}
Cluster URLs:

View File

@ -22,11 +22,22 @@ import typing
from decimal import Decimal
from solana.publickey import PublicKey
from .client import BetterClient, ClusterUrlData, TransactionStatusCollector, NullTransactionStatusCollector
from .client import (
BetterClient,
ClusterUrlData,
TransactionStatusCollector,
NullTransactionStatusCollector,
)
from .constants import MangoConstants
from .context import Context
from .idsjsonmarketlookup import IdsJsonMarketLookup
from .instrumentlookup import InstrumentLookup, CompoundInstrumentLookup, IdsJsonTokenLookup, NonSPLInstrumentLookup, SPLTokenLookup
from .instrumentlookup import (
InstrumentLookup,
CompoundInstrumentLookup,
IdsJsonTokenLookup,
NonSPLInstrumentLookup,
SPLTokenLookup,
)
from .marketlookup import CompoundMarketLookup, MarketLookup
from .serummarketlookup import SerumMarketLookup
@ -60,16 +71,24 @@ class ContextBuilder:
class ParseClusterUrls(argparse.Action):
cluster_urls: typing.List[ClusterUrlData] = []
def __call__(self, parser: argparse.ArgumentParser, namespace: object, values: typing.Any, option_string: typing.Optional[str] = None) -> None:
def __call__(
self,
parser: argparse.ArgumentParser,
namespace: object,
values: typing.Any,
option_string: typing.Optional[str] = None,
) -> None:
if values:
if len(values) == 1:
self.cluster_urls.append(ClusterUrlData(rpc=values[0]))
elif len(values) == 2:
self.cluster_urls.append(ClusterUrlData(rpc=values[0], ws=values[1]))
self.cluster_urls.append(
ClusterUrlData(rpc=values[0], ws=values[1])
)
else:
raise parser.error(
'Argument --cluster-url permits maximal two parameters. The first one configures HTTP connection url, the second one '
'configures the WS connection url. Example: --cluster-url https://localhost:8181 wss://localhost:8282'
"Argument --cluster-url permits maximal two parameters. The first one configures HTTP connection url, the second one "
"configures the WS connection url. Example: --cluster-url https://localhost:8181 wss://localhost:8282"
)
setattr(namespace, self.dest, self.cluster_urls)
@ -80,33 +99,95 @@ class ContextBuilder:
#
@staticmethod
def add_command_line_parameters(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--name", type=str, default="Mango Explorer",
help="Name of the program (used in reports and alerts)")
parser.add_argument("--cluster-name", type=str, default=None, help="Solana RPC cluster name")
parser.add_argument("--cluster-url", nargs='*', type=str, action=ContextBuilder.ParseClusterUrls, default=[],
help="Solana RPC cluster URL (can be specified multiple times to provide failover when one errors; optional second parameter value defines websocket connection)")
parser.add_argument("--group-name", type=str, default=None, help="Mango group name")
parser.add_argument("--group-address", type=PublicKey, default=None, help="Mango group address")
parser.add_argument("--mango-program-address", type=PublicKey, default=None, help="Mango program address")
parser.add_argument("--serum-program-address", type=PublicKey, default=None, help="Serum program address")
parser.add_argument("--skip-preflight", default=False, action="store_true", help="Skip pre-flight checks")
parser.add_argument("--commitment", type=str, default=None,
help="Commitment to use when sending transactions (can be 'finalized', 'confirmed' or 'processed')")
parser.add_argument("--encoding", type=str, default=None,
help="Encoding to request when receiving data from Solana (options are 'base58' (slow), 'base64', 'base64+zstd', or 'jsonParsed')")
parser.add_argument("--blockhash-cache-duration", type=int,
help="How long (in seconds) to cache 'recent' blockhashes")
parser.add_argument("--http-request-timeout", type=float, default=20,
help="What is the timeout for HTTP requests to when calling to RPC nodes (in seconds), -1 means no timeout")
parser.add_argument("--stale-data-pause-before-retry", type=Decimal,
help="How long (in seconds, e.g. 0.1) to pause after retrieving stale data before retrying")
parser.add_argument("--stale-data-maximum-retries", type=int,
help="How many times to retry fetching data after being given stale data before giving up")
parser.add_argument("--gma-chunk-size", type=Decimal, default=None,
help="Maximum number of addresses to send in a single call to getMultipleAccounts()")
parser.add_argument("--gma-chunk-pause", type=Decimal, default=None,
help="number of seconds to pause between successive getMultipleAccounts() calls to avoid rate limiting")
parser.add_argument("--reflink", type=PublicKey, default=None, help="Referral public key")
parser.add_argument(
"--name",
type=str,
default="Mango Explorer",
help="Name of the program (used in reports and alerts)",
)
parser.add_argument(
"--cluster-name", type=str, default=None, help="Solana RPC cluster name"
)
parser.add_argument(
"--cluster-url",
nargs="*",
type=str,
action=ContextBuilder.ParseClusterUrls,
default=[],
help="Solana RPC cluster URL (can be specified multiple times to provide failover when one errors; optional second parameter value defines websocket connection)",
)
parser.add_argument(
"--group-name", type=str, default=None, help="Mango group name"
)
parser.add_argument(
"--group-address", type=PublicKey, default=None, help="Mango group address"
)
parser.add_argument(
"--mango-program-address",
type=PublicKey,
default=None,
help="Mango program address",
)
parser.add_argument(
"--serum-program-address",
type=PublicKey,
default=None,
help="Serum program address",
)
parser.add_argument(
"--skip-preflight",
default=False,
action="store_true",
help="Skip pre-flight checks",
)
parser.add_argument(
"--commitment",
type=str,
default=None,
help="Commitment to use when sending transactions (can be 'finalized', 'confirmed' or 'processed')",
)
parser.add_argument(
"--encoding",
type=str,
default=None,
help="Encoding to request when receiving data from Solana (options are 'base58' (slow), 'base64', 'base64+zstd', or 'jsonParsed')",
)
parser.add_argument(
"--blockhash-cache-duration",
type=int,
help="How long (in seconds) to cache 'recent' blockhashes",
)
parser.add_argument(
"--http-request-timeout",
type=float,
default=20,
help="What is the timeout for HTTP requests to when calling to RPC nodes (in seconds), -1 means no timeout",
)
parser.add_argument(
"--stale-data-pause-before-retry",
type=Decimal,
help="How long (in seconds, e.g. 0.1) to pause after retrieving stale data before retrying",
)
parser.add_argument(
"--stale-data-maximum-retries",
type=int,
help="How many times to retry fetching data after being given stale data before giving up",
)
parser.add_argument(
"--gma-chunk-size",
type=Decimal,
default=None,
help="Maximum number of addresses to send in a single call to getMultipleAccounts()",
)
parser.add_argument(
"--gma-chunk-pause",
type=Decimal,
default=None,
help="number of seconds to pause between successive getMultipleAccounts() calls to avoid rate limiting",
)
parser.add_argument(
"--reflink", type=PublicKey, default=None, help="Referral public key"
)
# This function is the converse of `add_command_line_parameters()` - it takes
# an argument of parsed command-line parameters and expects to see the ones it added
@ -118,7 +199,9 @@ class ContextBuilder:
def from_command_line_parameters(args: argparse.Namespace) -> Context:
name: typing.Optional[str] = args.name
cluster_name: typing.Optional[str] = args.cluster_name
cluster_urls: typing.Optional[typing.Sequence[ClusterUrlData]] = args.cluster_url
cluster_urls: typing.Optional[
typing.Sequence[ClusterUrlData]
] = args.cluster_url
group_name: typing.Optional[str] = args.group_name
group_address: typing.Optional[PublicKey] = args.group_address
mango_program_address: typing.Optional[PublicKey] = args.mango_program_address
@ -128,8 +211,12 @@ class ContextBuilder:
encoding: typing.Optional[str] = args.encoding
blockhash_cache_duration: typing.Optional[int] = args.blockhash_cache_duration
http_request_timeout: typing.Optional[float] = args.http_request_timeout
stale_data_pause_before_retry: typing.Optional[Decimal] = args.stale_data_pause_before_retry
stale_data_maximum_retries: typing.Optional[int] = args.stale_data_maximum_retries
stale_data_pause_before_retry: typing.Optional[
Decimal
] = args.stale_data_pause_before_retry
stale_data_maximum_retries: typing.Optional[
int
] = args.stale_data_maximum_retries
gma_chunk_size: typing.Optional[Decimal] = args.gma_chunk_size
gma_chunk_pause: typing.Optional[Decimal] = args.gma_chunk_pause
reflink: typing.Optional[PublicKey] = args.reflink
@ -145,12 +232,24 @@ class ContextBuilder:
pause: Decimal = stale_data_pause_before_retry or Decimal("0.1")
actual_stale_data_pauses_before_retry = [float(pause)] * retries
context: Context = ContextBuilder.build(name, cluster_name, cluster_urls, skip_preflight, commitment,
encoding, blockhash_cache_duration, http_request_timeout,
actual_stale_data_pauses_before_retry,
group_name, group_address, mango_program_address,
serum_program_address, gma_chunk_size, gma_chunk_pause,
reflink)
context: Context = ContextBuilder.build(
name,
cluster_name,
cluster_urls,
skip_preflight,
commitment,
encoding,
blockhash_cache_duration,
http_request_timeout,
actual_stale_data_pauses_before_retry,
group_name,
group_address,
mango_program_address,
serum_program_address,
gma_chunk_size,
gma_chunk_pause,
reflink,
)
logging.debug(f"{context}")
return context
@ -161,73 +260,106 @@ class ContextBuilder:
@staticmethod
def from_group_name(context: Context, group_name: str) -> Context:
return ContextBuilder.build(context.name, context.client.cluster_name, context.client.cluster_urls,
context.client.skip_preflight, context.client.commitment,
context.client.encoding, context.client.blockhash_cache_duration, None,
context.client.stale_data_pauses_before_retry,
group_name, None, None, None,
context.gma_chunk_size, context.gma_chunk_pause,
context.reflink)
return ContextBuilder.build(
context.name,
context.client.cluster_name,
context.client.cluster_urls,
context.client.skip_preflight,
context.client.commitment,
context.client.encoding,
context.client.blockhash_cache_duration,
None,
context.client.stale_data_pauses_before_retry,
group_name,
None,
None,
None,
context.gma_chunk_size,
context.gma_chunk_pause,
context.reflink,
)
@staticmethod
def forced_to_devnet(context: Context) -> Context:
cluster_name: str = "devnet"
cluster_url: ClusterUrlData = ClusterUrlData(rpc=MangoConstants["cluster_urls"][cluster_name])
cluster_url: ClusterUrlData = ClusterUrlData(
rpc=MangoConstants["cluster_urls"][cluster_name]
)
fresh_context = copy.copy(context)
fresh_context.client = BetterClient.from_configuration(context.name,
cluster_name,
[cluster_url],
context.client.commitment,
context.client.skip_preflight,
context.client.encoding,
context.client.blockhash_cache_duration,
-1,
context.client.stale_data_pauses_before_retry,
context.client.instruction_reporter,
context.client.transaction_status_collector)
fresh_context.client = BetterClient.from_configuration(
context.name,
cluster_name,
[cluster_url],
context.client.commitment,
context.client.skip_preflight,
context.client.encoding,
context.client.blockhash_cache_duration,
-1,
context.client.stale_data_pauses_before_retry,
context.client.instruction_reporter,
context.client.transaction_status_collector,
)
return fresh_context
@staticmethod
def forced_to_mainnet_beta(context: Context) -> Context:
cluster_name: str = "mainnet"
cluster_url: ClusterUrlData = ClusterUrlData(rpc=MangoConstants["cluster_urls"][cluster_name])
cluster_url: ClusterUrlData = ClusterUrlData(
rpc=MangoConstants["cluster_urls"][cluster_name]
)
fresh_context = copy.copy(context)
fresh_context.client = BetterClient.from_configuration(context.name,
cluster_name,
[cluster_url],
context.client.commitment,
context.client.skip_preflight,
context.client.encoding,
context.client.blockhash_cache_duration,
-1,
context.client.stale_data_pauses_before_retry,
context.client.instruction_reporter,
context.client.transaction_status_collector)
fresh_context.client = BetterClient.from_configuration(
context.name,
cluster_name,
[cluster_url],
context.client.commitment,
context.client.skip_preflight,
context.client.encoding,
context.client.blockhash_cache_duration,
-1,
context.client.stale_data_pauses_before_retry,
context.client.instruction_reporter,
context.client.transaction_status_collector,
)
return fresh_context
@staticmethod
def build(name: typing.Optional[str] = None, cluster_name: typing.Optional[str] = None,
cluster_urls: typing.Optional[typing.Sequence[ClusterUrlData]] = None,
skip_preflight: bool = False,
commitment: typing.Optional[str] = None, encoding: typing.Optional[str] = None,
blockhash_cache_duration: typing.Optional[int] = None,
http_request_timeout: typing.Optional[float] = None,
stale_data_pauses_before_retry: typing.Optional[typing.Sequence[float]] = None,
group_name: typing.Optional[str] = None, group_address: typing.Optional[PublicKey] = None,
program_address: typing.Optional[PublicKey] = None, serum_program_address: typing.Optional[PublicKey] = None,
gma_chunk_size: typing.Optional[Decimal] = None, gma_chunk_pause: typing.Optional[Decimal] = None,
reflink: typing.Optional[PublicKey] = None,
transaction_status_collector: TransactionStatusCollector = NullTransactionStatusCollector()) -> "Context":
def __public_key_or_none(address: typing.Optional[str]) -> typing.Optional[PublicKey]:
def build(
name: typing.Optional[str] = None,
cluster_name: typing.Optional[str] = None,
cluster_urls: typing.Optional[typing.Sequence[ClusterUrlData]] = None,
skip_preflight: bool = False,
commitment: typing.Optional[str] = None,
encoding: typing.Optional[str] = None,
blockhash_cache_duration: typing.Optional[int] = None,
http_request_timeout: typing.Optional[float] = None,
stale_data_pauses_before_retry: typing.Optional[typing.Sequence[float]] = None,
group_name: typing.Optional[str] = None,
group_address: typing.Optional[PublicKey] = None,
program_address: typing.Optional[PublicKey] = None,
serum_program_address: typing.Optional[PublicKey] = None,
gma_chunk_size: typing.Optional[Decimal] = None,
gma_chunk_pause: typing.Optional[Decimal] = None,
reflink: typing.Optional[PublicKey] = None,
transaction_status_collector: TransactionStatusCollector = NullTransactionStatusCollector(),
) -> "Context":
def __public_key_or_none(
address: typing.Optional[str],
) -> typing.Optional[PublicKey]:
if address is not None and address != "":
return PublicKey(address)
return None
# The first group is only used to determine the default cluster if it is not otherwise specified.
first_group_data = MangoConstants["groups"][0]
actual_name: str = name or os.environ.get("NAME") or "Mango Explorer"
actual_cluster: str = cluster_name or os.environ.get("CLUSTER_NAME") or first_group_data["cluster"]
actual_cluster: str = (
cluster_name
or os.environ.get("CLUSTER_NAME")
or first_group_data["cluster"]
)
# Now that we have the actual cluster name, taking environment variables and defaults into account,
# we can decide what we want as the default group.
@ -239,42 +371,72 @@ class ContextBuilder:
actual_commitment: str = commitment or "processed"
actual_encoding: str = encoding or "base64"
actual_blockhash_cache_duration: int = blockhash_cache_duration or 0
actual_stale_data_pauses_before_retry: typing.Sequence[float] = stale_data_pauses_before_retry or []
actual_stale_data_pauses_before_retry: typing.Sequence[float] = (
stale_data_pauses_before_retry or []
)
actual_http_request_timeout: float = http_request_timeout or -1
actual_cluster_urls: typing.Optional[typing.Sequence[ClusterUrlData]] = cluster_urls
actual_cluster_urls: typing.Optional[
typing.Sequence[ClusterUrlData]
] = cluster_urls
if actual_cluster_urls is None or len(actual_cluster_urls) == 0:
cluster_url_from_environment: typing.Optional[str] = os.environ.get("CLUSTER_URL")
if cluster_url_from_environment is not None and cluster_url_from_environment != "":
cluster_url_from_environment: typing.Optional[str] = os.environ.get(
"CLUSTER_URL"
)
if (
cluster_url_from_environment is not None
and cluster_url_from_environment != ""
):
actual_cluster_urls = [ClusterUrlData(rpc=cluster_url_from_environment)]
else:
actual_cluster_urls = [ClusterUrlData(rpc=MangoConstants["cluster_urls"][actual_cluster])]
actual_cluster_urls = [
ClusterUrlData(rpc=MangoConstants["cluster_urls"][actual_cluster])
]
actual_skip_preflight: bool = skip_preflight
actual_group_name: str = group_name or os.environ.get("GROUP_NAME") or default_group_data["name"]
actual_group_name: str = (
group_name or os.environ.get("GROUP_NAME") or default_group_data["name"]
)
found_group_data: typing.Any = None
for group in MangoConstants["groups"]:
if group["cluster"] == actual_cluster and group["name"].upper() == actual_group_name.upper():
if (
group["cluster"] == actual_cluster
and group["name"].upper() == actual_group_name.upper()
):
found_group_data = group
if found_group_data is None:
raise Exception(f"Could not find group named '{actual_group_name}' in cluster '{actual_cluster}'.")
raise Exception(
f"Could not find group named '{actual_group_name}' in cluster '{actual_cluster}'."
)
actual_group_address: PublicKey = group_address or __public_key_or_none(os.environ.get(
"GROUP_ADDRESS")) or PublicKey(found_group_data["publicKey"])
actual_program_address: PublicKey = program_address or __public_key_or_none(os.environ.get(
"MANGO_PROGRAM_ADDRESS")) or PublicKey(found_group_data["mangoProgramId"])
actual_serum_program_address: PublicKey = serum_program_address or __public_key_or_none(os.environ.get(
"SERUM_PROGRAM_ADDRESS")) or PublicKey(found_group_data["serumProgramId"])
actual_group_address: PublicKey = (
group_address
or __public_key_or_none(os.environ.get("GROUP_ADDRESS"))
or PublicKey(found_group_data["publicKey"])
)
actual_program_address: PublicKey = (
program_address
or __public_key_or_none(os.environ.get("MANGO_PROGRAM_ADDRESS"))
or PublicKey(found_group_data["mangoProgramId"])
)
actual_serum_program_address: PublicKey = (
serum_program_address
or __public_key_or_none(os.environ.get("SERUM_PROGRAM_ADDRESS"))
or PublicKey(found_group_data["serumProgramId"])
)
actual_gma_chunk_size: Decimal = gma_chunk_size or Decimal(100)
actual_gma_chunk_pause: Decimal = gma_chunk_pause or Decimal(0)
actual_reflink: typing.Optional[PublicKey] = reflink or __public_key_or_none(
os.environ.get("MANGO_REFLINK_ADDRESS"))
os.environ.get("MANGO_REFLINK_ADDRESS")
)
ids_json_token_lookup: InstrumentLookup = IdsJsonTokenLookup(actual_cluster, actual_group_name)
ids_json_token_lookup: InstrumentLookup = IdsJsonTokenLookup(
actual_cluster, actual_group_name
)
instrument_lookup: InstrumentLookup = ids_json_token_lookup
if actual_cluster == "mainnet":
# 'Overrides' are for problematic situations.
@ -293,47 +455,103 @@ class ContextBuilder:
#
# 'Overrides' allows us to put the details we expect for 'ETH' into our loader, ahead of the SPL
# JSON, so that our code and users can continue to use, for example, ETH/USDT, as they expect.
mainnet_overrides_token_lookup: InstrumentLookup = SPLTokenLookup.load(SPLTokenLookup.OverridesDataFilepath)
mainnet_spl_token_lookup: InstrumentLookup = SPLTokenLookup.load(SPLTokenLookup.DefaultDataFilepath)
mainnet_non_spl_instrument_lookup: InstrumentLookup = NonSPLInstrumentLookup.load(
NonSPLInstrumentLookup.DefaultMainnetDataFilepath)
instrument_lookup = CompoundInstrumentLookup([
ids_json_token_lookup,
mainnet_overrides_token_lookup,
mainnet_non_spl_instrument_lookup,
mainnet_spl_token_lookup])
mainnet_overrides_token_lookup: InstrumentLookup = SPLTokenLookup.load(
SPLTokenLookup.OverridesDataFilepath
)
mainnet_spl_token_lookup: InstrumentLookup = SPLTokenLookup.load(
SPLTokenLookup.DefaultDataFilepath
)
mainnet_non_spl_instrument_lookup: InstrumentLookup = (
NonSPLInstrumentLookup.load(
NonSPLInstrumentLookup.DefaultMainnetDataFilepath
)
)
instrument_lookup = CompoundInstrumentLookup(
[
ids_json_token_lookup,
mainnet_overrides_token_lookup,
mainnet_non_spl_instrument_lookup,
mainnet_spl_token_lookup,
]
)
elif actual_cluster == "devnet":
devnet_overrides_token_lookup: InstrumentLookup = SPLTokenLookup.load(
SPLTokenLookup.DevnetOverridesDataFilepath)
devnet_spl_token_lookup: InstrumentLookup = SPLTokenLookup.load(SPLTokenLookup.DevnetDataFilepath)
devnet_non_spl_instrument_lookup: InstrumentLookup = NonSPLInstrumentLookup.load(
NonSPLInstrumentLookup.DefaultDevnetDataFilepath)
instrument_lookup = CompoundInstrumentLookup([
ids_json_token_lookup,
devnet_overrides_token_lookup,
devnet_non_spl_instrument_lookup,
devnet_spl_token_lookup])
SPLTokenLookup.DevnetOverridesDataFilepath
)
devnet_spl_token_lookup: InstrumentLookup = SPLTokenLookup.load(
SPLTokenLookup.DevnetDataFilepath
)
devnet_non_spl_instrument_lookup: InstrumentLookup = (
NonSPLInstrumentLookup.load(
NonSPLInstrumentLookup.DefaultDevnetDataFilepath
)
)
instrument_lookup = CompoundInstrumentLookup(
[
ids_json_token_lookup,
devnet_overrides_token_lookup,
devnet_non_spl_instrument_lookup,
devnet_spl_token_lookup,
]
)
ids_json_market_lookup: MarketLookup = IdsJsonMarketLookup(actual_cluster, instrument_lookup)
ids_json_market_lookup: MarketLookup = IdsJsonMarketLookup(
actual_cluster, instrument_lookup
)
all_market_lookup = ids_json_market_lookup
if actual_cluster == "mainnet":
mainnet_overrides_serum_market_lookup: SerumMarketLookup = SerumMarketLookup.load(
actual_serum_program_address, SPLTokenLookup.OverridesDataFilepath)
mainnet_overrides_serum_market_lookup: SerumMarketLookup = (
SerumMarketLookup.load(
actual_serum_program_address, SPLTokenLookup.OverridesDataFilepath
)
)
mainnet_serum_market_lookup: SerumMarketLookup = SerumMarketLookup.load(
actual_serum_program_address, SPLTokenLookup.DefaultDataFilepath)
all_market_lookup = CompoundMarketLookup([
ids_json_market_lookup,
mainnet_overrides_serum_market_lookup,
mainnet_serum_market_lookup])
actual_serum_program_address, SPLTokenLookup.DefaultDataFilepath
)
all_market_lookup = CompoundMarketLookup(
[
ids_json_market_lookup,
mainnet_overrides_serum_market_lookup,
mainnet_serum_market_lookup,
]
)
elif actual_cluster == "devnet":
devnet_overrides_serum_market_lookup: SerumMarketLookup = SerumMarketLookup.load(
actual_serum_program_address, SPLTokenLookup.DevnetOverridesDataFilepath)
devnet_overrides_serum_market_lookup: SerumMarketLookup = (
SerumMarketLookup.load(
actual_serum_program_address,
SPLTokenLookup.DevnetOverridesDataFilepath,
)
)
devnet_serum_market_lookup: SerumMarketLookup = SerumMarketLookup.load(
actual_serum_program_address, SPLTokenLookup.DevnetDataFilepath)
all_market_lookup = CompoundMarketLookup([
ids_json_market_lookup,
devnet_overrides_serum_market_lookup,
devnet_serum_market_lookup])
actual_serum_program_address, SPLTokenLookup.DevnetDataFilepath
)
all_market_lookup = CompoundMarketLookup(
[
ids_json_market_lookup,
devnet_overrides_serum_market_lookup,
devnet_serum_market_lookup,
]
)
market_lookup: MarketLookup = all_market_lookup
return Context(actual_name, actual_cluster, actual_cluster_urls, actual_skip_preflight, actual_commitment, actual_encoding, actual_blockhash_cache_duration, actual_http_request_timeout, actual_stale_data_pauses_before_retry, actual_program_address, actual_serum_program_address, actual_group_name, actual_group_address, actual_gma_chunk_size, actual_gma_chunk_pause, actual_reflink, instrument_lookup, market_lookup, transaction_status_collector)
return Context(
actual_name,
actual_cluster,
actual_cluster_urls,
actual_skip_preflight,
actual_commitment,
actual_encoding,
actual_blockhash_cache_duration,
actual_http_request_timeout,
actual_stale_data_pauses_before_retry,
actual_program_address,
actual_serum_program_address,
actual_group_name,
actual_group_address,
actual_gma_chunk_size,
actual_gma_chunk_pause,
actual_reflink,
instrument_lookup,
market_lookup,
transaction_status_collector,
)

View File

@ -19,7 +19,12 @@ from .account import Account
from .context import Context
from .ensuremarketloaded import ensure_market_loaded
from .market import Market
from .marketoperations import MarketInstructionBuilder, MarketOperations, NullMarketInstructionBuilder, NullMarketOperations
from .marketoperations import (
MarketInstructionBuilder,
MarketOperations,
NullMarketInstructionBuilder,
NullMarketOperations,
)
from .perpmarketoperations import PerpMarketInstructionBuilder, PerpMarketOperations
from .perpmarket import PerpMarket
from .serummarket import SerumMarket
@ -33,7 +38,13 @@ from .wallet import Wallet
#
# This function deals with the creation of a `MarketInstructionBuilder` object for a given `Market`.
#
def create_market_instruction_builder(context: Context, wallet: Wallet, account: Account, market: Market, dry_run: bool = False) -> MarketInstructionBuilder:
def create_market_instruction_builder(
context: Context,
wallet: Wallet,
account: Account,
market: Market,
dry_run: bool = False,
) -> MarketInstructionBuilder:
if dry_run:
return NullMarketInstructionBuilder(market.symbol)
@ -41,37 +52,66 @@ def create_market_instruction_builder(context: Context, wallet: Wallet, account:
if isinstance(loaded_market, SerumMarket):
return SerumMarketInstructionBuilder.load(context, wallet, loaded_market)
elif isinstance(loaded_market, SpotMarket):
return SpotMarketInstructionBuilder.load(context, wallet, loaded_market, loaded_market.group, account)
return SpotMarketInstructionBuilder.load(
context, wallet, loaded_market, loaded_market.group, account
)
elif isinstance(loaded_market, PerpMarket):
return PerpMarketInstructionBuilder.load(context, wallet, loaded_market, loaded_market.group, account)
return PerpMarketInstructionBuilder.load(
context, wallet, loaded_market, loaded_market.group, account
)
else:
raise Exception(f"Could not find market instructions builder for market {market.symbol}")
raise Exception(
f"Could not find market instructions builder for market {market.symbol}"
)
# # 🥭 create_market_operations
#
# This function deals with the creation of a `MarketOperations` object for a given `Market`.
#
def create_market_operations(context: Context, wallet: Wallet, account: typing.Optional[Account], market: Market, dry_run: bool = False) -> MarketOperations:
def create_market_operations(
context: Context,
wallet: Wallet,
account: typing.Optional[Account],
market: Market,
dry_run: bool = False,
) -> MarketOperations:
if dry_run:
return NullMarketOperations(market.symbol)
loaded_market: Market = ensure_market_loaded(context, market)
if isinstance(loaded_market, SerumMarket):
serum_market_instruction_builder: SerumMarketInstructionBuilder = SerumMarketInstructionBuilder.load(
context, wallet, loaded_market)
serum_market_instruction_builder: SerumMarketInstructionBuilder = (
SerumMarketInstructionBuilder.load(context, wallet, loaded_market)
)
return SerumMarketOperations(context, wallet, serum_market_instruction_builder)
elif isinstance(loaded_market, SpotMarket):
if account is None:
raise Exception("Account is required for SpotMarket operations.")
spot_market_instruction_builder: SpotMarketInstructionBuilder = SpotMarketInstructionBuilder.load(
context, wallet, loaded_market, loaded_market.group, account)
return SpotMarketOperations(context, wallet, account, spot_market_instruction_builder)
spot_market_instruction_builder: SpotMarketInstructionBuilder = (
SpotMarketInstructionBuilder.load(
context, wallet, loaded_market, loaded_market.group, account
)
)
return SpotMarketOperations(
context, wallet, account, spot_market_instruction_builder
)
elif isinstance(loaded_market, PerpMarket):
if account is None:
raise Exception("Account is required for PerpMarket operations.")
perp_market_instruction_builder: PerpMarketInstructionBuilder = PerpMarketInstructionBuilder.load(
context, wallet, loaded_market, loaded_market.underlying_perp_market.group, account)
return PerpMarketOperations(context, wallet, account, perp_market_instruction_builder)
perp_market_instruction_builder: PerpMarketInstructionBuilder = (
PerpMarketInstructionBuilder.load(
context,
wallet,
loaded_market,
loaded_market.underlying_perp_market.group,
account,
)
)
return PerpMarketOperations(
context, wallet, account, perp_market_instruction_builder
)
else:
raise Exception(f"Could not find market operations handler for market {market.symbol}")
raise Exception(
f"Could not find market operations handler for market {market.symbol}"
)

View File

@ -72,4 +72,4 @@ def encode_key(key: PublicKey) -> str:
#
# Encodes an `int` in the proper way for RPC calls.
def encode_int(value: int) -> str:
return base58.b58encode_int(value).decode('ascii')
return base58.b58encode_int(value).decode("ascii")

View File

@ -38,7 +38,14 @@ from .version import Version
# # 🥭 GroupSlotSpotMarket class
#
class GroupSlotSpotMarket:
def __init__(self, address: PublicKey, maint_asset_weight: Decimal, init_asset_weight: Decimal, maint_liab_weight: Decimal, init_liab_weight: Decimal) -> None:
def __init__(
self,
address: PublicKey,
maint_asset_weight: Decimal,
init_asset_weight: Decimal,
maint_liab_weight: Decimal,
init_liab_weight: Decimal,
) -> None:
self.address: PublicKey = address
self.maint_asset_weight: Decimal = maint_asset_weight
self.init_asset_weight: Decimal = init_asset_weight
@ -52,11 +59,21 @@ class GroupSlotSpotMarket:
init_asset_weight: Decimal = round(layout.init_asset_weight, 8)
maint_liab_weight: Decimal = round(layout.maint_liab_weight, 8)
init_liab_weight: Decimal = round(layout.init_liab_weight, 8)
return GroupSlotSpotMarket(spot_market, maint_asset_weight, init_asset_weight, maint_liab_weight, init_liab_weight)
return GroupSlotSpotMarket(
spot_market,
maint_asset_weight,
init_asset_weight,
maint_liab_weight,
init_liab_weight,
)
@staticmethod
def from_layout_or_none(layout: typing.Any) -> typing.Optional["GroupSlotSpotMarket"]:
if (layout.spot_market is None) or (layout.spot_market == SYSTEM_PROGRAM_ADDRESS):
def from_layout_or_none(
layout: typing.Any,
) -> typing.Optional["GroupSlotSpotMarket"]:
if (layout.spot_market is None) or (
layout.spot_market == SYSTEM_PROGRAM_ADDRESS
):
return None
return GroupSlotSpotMarket.from_layout(layout)
@ -78,7 +95,17 @@ class GroupSlotSpotMarket:
# # 🥭 GroupSlotPerpMarket class
#
class GroupSlotPerpMarket:
def __init__(self, address: PublicKey, maint_asset_weight: Decimal, init_asset_weight: Decimal, maint_liab_weight: Decimal, init_liab_weight: Decimal, liquidation_fee: Decimal, base_lot_size: Decimal, quote_lot_size: Decimal) -> None:
def __init__(
self,
address: PublicKey,
maint_asset_weight: Decimal,
init_asset_weight: Decimal,
maint_liab_weight: Decimal,
init_liab_weight: Decimal,
liquidation_fee: Decimal,
base_lot_size: Decimal,
quote_lot_size: Decimal,
) -> None:
self.address: PublicKey = address
self.maint_asset_weight: Decimal = maint_asset_weight
self.init_asset_weight: Decimal = init_asset_weight
@ -99,11 +126,24 @@ class GroupSlotPerpMarket:
base_lot_size: Decimal = layout.base_lot_size
quote_lot_size: Decimal = layout.quote_lot_size
return GroupSlotPerpMarket(perp_market, maint_asset_weight, init_asset_weight, maint_liab_weight, init_liab_weight, liquidation_fee, base_lot_size, quote_lot_size)
return GroupSlotPerpMarket(
perp_market,
maint_asset_weight,
init_asset_weight,
maint_liab_weight,
init_liab_weight,
liquidation_fee,
base_lot_size,
quote_lot_size,
)
@staticmethod
def from_layout_or_none(layout: typing.Any) -> typing.Optional["GroupSlotPerpMarket"]:
if (layout.perp_market is None) or (layout.perp_market == SYSTEM_PROGRAM_ADDRESS):
def from_layout_or_none(
layout: typing.Any,
) -> typing.Optional["GroupSlotPerpMarket"]:
if (layout.perp_market is None) or (
layout.perp_market == SYSTEM_PROGRAM_ADDRESS
):
return None
return GroupSlotPerpMarket.from_layout(layout)
@ -130,7 +170,17 @@ class GroupSlotPerpMarket:
# `GroupSlot` gathers indexed slot items together instead of separate arrays.
#
class GroupSlot:
def __init__(self, index: int, base_instrument: Instrument, base_token_bank: typing.Optional[TokenBank], quote_token_bank: TokenBank, spot_market_info: typing.Optional[GroupSlotSpotMarket], perp_market_info: typing.Optional[GroupSlotPerpMarket], perp_lot_size_converter: LotSizeConverter, oracle: PublicKey) -> None:
def __init__(
self,
index: int,
base_instrument: Instrument,
base_token_bank: typing.Optional[TokenBank],
quote_token_bank: TokenBank,
spot_market_info: typing.Optional[GroupSlotSpotMarket],
perp_market_info: typing.Optional[GroupSlotPerpMarket],
perp_lot_size_converter: LotSizeConverter,
oracle: PublicKey,
) -> None:
self.index: int = index
self.base_instrument: Instrument = base_instrument
self.base_token_bank: typing.Optional[TokenBank] = base_token_bank
@ -174,17 +224,31 @@ class GroupSlot:
# `Group` defines root functionality for Mango Markets.
#
class Group(AddressableAccount):
def __init__(self, account_info: AccountInfo, version: Version, name: str,
meta_data: Metadata,
shared_quote: TokenBank,
slot_indices: typing.Sequence[bool],
slots: typing.Sequence[GroupSlot],
signer_nonce: Decimal, signer_key: PublicKey,
admin: PublicKey, serum_program_address: PublicKey, cache: PublicKey, valid_interval: Decimal,
insurance_vault: PublicKey, srm_vault: PublicKey, msrm_vault: PublicKey, fees_vault: PublicKey,
max_mango_accounts: Decimal, num_mango_accounts: Decimal,
referral_surcharge_centibps: Decimal, referral_share_centibps: Decimal,
referral_mngo_required: Decimal) -> None:
def __init__(
self,
account_info: AccountInfo,
version: Version,
name: str,
meta_data: Metadata,
shared_quote: TokenBank,
slot_indices: typing.Sequence[bool],
slots: typing.Sequence[GroupSlot],
signer_nonce: Decimal,
signer_key: PublicKey,
admin: PublicKey,
serum_program_address: PublicKey,
cache: PublicKey,
valid_interval: Decimal,
insurance_vault: PublicKey,
srm_vault: PublicKey,
msrm_vault: PublicKey,
fees_vault: PublicKey,
max_mango_accounts: Decimal,
num_mango_accounts: Decimal,
referral_surcharge_centibps: Decimal,
referral_share_centibps: Decimal,
referral_mngo_required: Decimal,
) -> None:
super().__init__(account_info)
self.version: Version = version
self.name: str = name
@ -219,7 +283,9 @@ class Group(AddressableAccount):
if token_bank.token.symbol_matches("MNGO"):
return token_bank
raise Exception(f"Could not find token info for symbol 'MNGO' in group {self.address}")
raise Exception(
f"Could not find token info for symbol 'MNGO' in group {self.address}"
)
@property
def liquidity_incentive_token(self) -> Token:
@ -248,11 +314,18 @@ class Group(AddressableAccount):
@property
def base_tokens(self) -> typing.Sequence[TokenBank]:
return [slot.base_token_bank for slot in self.slots if slot.base_token_bank is not None]
return [
slot.base_token_bank
for slot in self.slots
if slot.base_token_bank is not None
]
@property
def base_tokens_by_index(self) -> typing.Sequence[typing.Optional[TokenBank]]:
return [slot.base_token_bank if slot is not None else None for slot in self.slots_by_index]
return [
slot.base_token_bank if slot is not None else None
for slot in self.slots_by_index
]
@property
def oracles(self) -> typing.Sequence[PublicKey]:
@ -260,29 +333,49 @@ class Group(AddressableAccount):
@property
def oracles_by_index(self) -> typing.Sequence[typing.Optional[PublicKey]]:
return [slot.oracle if slot is not None else None for slot in self.slots_by_index]
return [
slot.oracle if slot is not None else None for slot in self.slots_by_index
]
@property
def spot_markets(self) -> typing.Sequence[GroupSlotSpotMarket]:
return [slot.spot_market for slot in self.slots if slot.spot_market is not None]
@property
def spot_markets_by_index(self) -> typing.Sequence[typing.Optional[GroupSlotSpotMarket]]:
return [slot.spot_market if slot is not None else None for slot in self.slots_by_index]
def spot_markets_by_index(
self,
) -> typing.Sequence[typing.Optional[GroupSlotSpotMarket]]:
return [
slot.spot_market if slot is not None else None
for slot in self.slots_by_index
]
@property
def perp_markets(self) -> typing.Sequence[GroupSlotPerpMarket]:
return [slot.perp_market for slot in self.slots if slot.perp_market is not None]
@property
def perp_markets_by_index(self) -> typing.Sequence[typing.Optional[GroupSlotPerpMarket]]:
return [slot.perp_market if slot is not None else None for slot in self.slots_by_index]
def perp_markets_by_index(
self,
) -> typing.Sequence[typing.Optional[GroupSlotPerpMarket]]:
return [
slot.perp_market if slot is not None else None
for slot in self.slots_by_index
]
@staticmethod
def from_layout(layout: typing.Any, name: str, account_info: AccountInfo, version: Version, instrument_lookup: InstrumentLookup, market_lookup: MarketLookup) -> "Group":
def from_layout(
layout: typing.Any,
name: str,
account_info: AccountInfo,
version: Version,
instrument_lookup: InstrumentLookup,
market_lookup: MarketLookup,
) -> "Group":
meta_data: Metadata = Metadata.from_layout(layout.meta_data)
tokens: typing.List[typing.Optional[TokenBank]] = [
TokenBank.from_layout_or_none(t, instrument_lookup) for t in layout.tokens]
TokenBank.from_layout_or_none(t, instrument_lookup) for t in layout.tokens
]
# By convention, the shared quote token is always at the end.
quote_token_bank: typing.Optional[TokenBank] = tokens[-1]
@ -291,10 +384,12 @@ class Group(AddressableAccount):
slots: typing.List[GroupSlot] = []
in_slots: typing.List[bool] = []
for index in range(len(tokens) - 1):
spot_market_info: typing.Optional[GroupSlotSpotMarket] = GroupSlotSpotMarket.from_layout_or_none(
layout.spot_markets[index])
perp_market_info: typing.Optional[GroupSlotPerpMarket] = GroupSlotPerpMarket.from_layout_or_none(
layout.perp_markets[index])
spot_market_info: typing.Optional[
GroupSlotSpotMarket
] = GroupSlotSpotMarket.from_layout_or_none(layout.spot_markets[index])
perp_market_info: typing.Optional[
GroupSlotPerpMarket
] = GroupSlotPerpMarket.from_layout_or_none(layout.perp_markets[index])
if (spot_market_info is None) and (perp_market_info is None):
in_slots += [False]
else:
@ -306,23 +401,40 @@ class Group(AddressableAccount):
else:
# It's possible there's no underlying SPL token and we have a pure PERP market.
if perp_market_info is None:
raise Exception(f"Cannot find base token or perp market info for index {index}")
perp_market = market_lookup.find_by_address(perp_market_info.address)
raise Exception(
f"Cannot find base token or perp market info for index {index}"
)
perp_market = market_lookup.find_by_address(
perp_market_info.address
)
if perp_market is None:
in_slots += [False]
logging.warning(
f"Group cannot find base token or perp market for index {index} - {perp_market_info}")
f"Group cannot find base token or perp market for index {index} - {perp_market_info}"
)
continue
base_instrument = perp_market.base
if perp_market_info is not None:
perp_lot_size_converter = LotSizeConverter(
base_instrument, perp_market_info.base_lot_size, quote_token_bank.token, perp_market_info.quote_lot_size)
base_instrument,
perp_market_info.base_lot_size,
quote_token_bank.token,
perp_market_info.quote_lot_size,
)
oracle: PublicKey = layout.oracles[index]
slot: GroupSlot = GroupSlot(index, base_instrument, base_token_bank, quote_token_bank,
spot_market_info, perp_market_info, perp_lot_size_converter, oracle)
slot: GroupSlot = GroupSlot(
index,
base_instrument,
base_token_bank,
quote_token_bank,
spot_market_info,
perp_market_info,
perp_lot_size_converter,
oracle,
)
slots += [slot]
in_slots += [True]
@ -343,22 +455,55 @@ class Group(AddressableAccount):
referral_share_centibps: Decimal = layout.referral_share_centibps
referral_mngo_required: Decimal = layout.referral_mngo_required
return Group(account_info, version, name, meta_data, quote_token_bank, in_slots, slots, signer_nonce, signer_key, admin, serum_program_address, cache_address, valid_interval, insurance_vault, srm_vault, msrm_vault, fees_vault, max_mango_accounts, num_mango_accounts, referral_surcharge_centibps, referral_share_centibps, referral_mngo_required)
return Group(
account_info,
version,
name,
meta_data,
quote_token_bank,
in_slots,
slots,
signer_nonce,
signer_key,
admin,
serum_program_address,
cache_address,
valid_interval,
insurance_vault,
srm_vault,
msrm_vault,
fees_vault,
max_mango_accounts,
num_mango_accounts,
referral_surcharge_centibps,
referral_share_centibps,
referral_mngo_required,
)
@staticmethod
def parse(account_info: AccountInfo, name: str, instrument_lookup: InstrumentLookup, market_lookup: MarketLookup) -> "Group":
def parse(
account_info: AccountInfo,
name: str,
instrument_lookup: InstrumentLookup,
market_lookup: MarketLookup,
) -> "Group":
data = account_info.data
if len(data) != layouts.GROUP.sizeof():
raise Exception(
f"Group data length ({len(data)}) does not match expected size ({layouts.GROUP.sizeof()})")
f"Group data length ({len(data)}) does not match expected size ({layouts.GROUP.sizeof()})"
)
layout = layouts.GROUP.parse(data)
return Group.from_layout(layout, name, account_info, Version.V3, instrument_lookup, market_lookup)
return Group.from_layout(
layout, name, account_info, Version.V3, instrument_lookup, market_lookup
)
@staticmethod
def parse_with_context(context: Context, account_info: AccountInfo) -> "Group":
name = context.lookup_group_name(account_info.address)
return Group.parse(account_info, name, context.instrument_lookup, context.market_lookup)
return Group.parse(
account_info, name, context.instrument_lookup, context.market_lookup
)
@staticmethod
def load(context: Context, address: typing.Optional[PublicKey] = None) -> "Group":
@ -368,23 +513,37 @@ class Group(AddressableAccount):
raise Exception(f"Group account not found at address '{group_address}'")
name = context.lookup_group_name(account_info.address)
return Group.parse(account_info, name, context.instrument_lookup, context.market_lookup)
return Group.parse(
account_info, name, context.instrument_lookup, context.market_lookup
)
def slot_by_spot_market_address(self, spot_market_address: PublicKey) -> GroupSlot:
for slot in self.slots:
if slot.spot_market is not None and slot.spot_market.address == spot_market_address:
if (
slot.spot_market is not None
and slot.spot_market.address == spot_market_address
):
return slot
raise Exception(f"Could not find spot market {spot_market_address} in group {self.address}")
raise Exception(
f"Could not find spot market {spot_market_address} in group {self.address}"
)
def slot_by_perp_market_address(self, perp_market_address: PublicKey) -> GroupSlot:
for slot in self.slots:
if slot.perp_market is not None and slot.perp_market.address == perp_market_address:
if (
slot.perp_market is not None
and slot.perp_market.address == perp_market_address
):
return slot
raise Exception(f"Could not find perp market {perp_market_address} in group {self.address}")
raise Exception(
f"Could not find perp market {perp_market_address} in group {self.address}"
)
def slot_by_instrument_or_none(self, instrument: Instrument) -> typing.Optional[GroupSlot]:
def slot_by_instrument_or_none(
self, instrument: Instrument
) -> typing.Optional[GroupSlot]:
for slot in self.slots:
if slot.base_instrument == instrument:
return slot
@ -405,7 +564,9 @@ class Group(AddressableAccount):
raise Exception(f"Could not find token {instrument} in group {self.address}")
def token_price_from_cache(self, cache: Cache, token: Instrument) -> InstrumentValue:
def token_price_from_cache(
self, cache: Cache, token: Instrument
) -> InstrumentValue:
if token == self.shared_quote_token:
# 1 USDC is always worth 1 USDC
return InstrumentValue(self.shared_quote_token, Decimal(1))
@ -413,22 +574,32 @@ class Group(AddressableAccount):
market_cache: MarketCache = self.market_cache_from_cache(cache, token)
return market_cache.adjusted_price(token, self.shared_quote_token)
def perp_market_cache_from_cache(self, cache: Cache, token: Instrument) -> typing.Optional[PerpMarketCache]:
def perp_market_cache_from_cache(
self, cache: Cache, token: Instrument
) -> typing.Optional[PerpMarketCache]:
market_cache: MarketCache = self.market_cache_from_cache(cache, token)
return market_cache.perp_market
def market_cache_from_cache_or_none(self, cache: Cache, instrument: Instrument) -> typing.Optional[MarketCache]:
def market_cache_from_cache_or_none(
self, cache: Cache, instrument: Instrument
) -> typing.Optional[MarketCache]:
slot: typing.Optional[GroupSlot] = self.slot_by_instrument_or_none(instrument)
if slot is None:
return None
instrument_index: int = slot.index
return cache.market_cache_for_index(instrument_index)
def market_cache_from_cache(self, cache: Cache, instrument: Instrument) -> MarketCache:
market_cache: typing.Optional[MarketCache] = self.market_cache_from_cache_or_none(cache, instrument)
def market_cache_from_cache(
self, cache: Cache, instrument: Instrument
) -> MarketCache:
market_cache: typing.Optional[
MarketCache
] = self.market_cache_from_cache_or_none(cache, instrument)
if market_cache is not None:
return market_cache
raise Exception(f"Could not find market cache for instrument {instrument.symbol}")
raise Exception(
f"Could not find market cache for instrument {instrument.symbol}"
)
def fetch_cache(self, context: Context) -> Cache:
return Cache.load(context, self.cache)
@ -437,26 +608,26 @@ class Group(AddressableAccount):
if not isinstance(id, str):
raise Exception(f"Referrer ID '{id}' is not a string")
id_bytes = id.encode('utf-8')
id_bytes = id.encode("utf-8")
if len(id_bytes) > 32:
raise Exception(f"Referrer ID '{id}' is too long - maximum is 32 bytes")
id_bytes_padded = id_bytes.ljust(32, b"\0")
referrer_record_address_and_nonce: typing.Tuple[PublicKey, int] = PublicKey.find_program_address(
[
bytes(self.address),
b"ReferrerIdRecord",
id_bytes_padded
],
context.mango_program_address
referrer_record_address_and_nonce: typing.Tuple[
PublicKey, int
] = PublicKey.find_program_address(
[bytes(self.address), b"ReferrerIdRecord", id_bytes_padded],
context.mango_program_address,
)
return referrer_record_address_and_nonce[0]
def __str__(self) -> str:
slot_count = len(self.slots)
slots = "\n ".join([f"{item}".replace("\n", "\n ") for item in self.slots])
slots = "\n ".join(
[f"{item}".replace("\n", "\n ") for item in self.slots]
)
return f"""« Group {self.version} [{self.address}]
{self.meta_data}
Name: {self.name}

View File

@ -32,11 +32,14 @@ class HealthCheck(rx.core.typing.Disposable):
def add(self, name: str, observable: rx.core.typing.Observable[typing.Any]) -> None:
healthcheck_file_touch_disposer = observable.subscribe(
on_next=lambda _: self.ping(name)) # type: ignore[call-arg]
on_next=lambda _: self.ping(name)
) # type: ignore[call-arg]
self._to_dispose += [healthcheck_file_touch_disposer]
def ping(self, name: str) -> None:
Path(f"{self.healthcheck_files_location}/mango_healthcheck_{name}").touch(mode=0o666, exist_ok=True)
Path(f"{self.healthcheck_files_location}/mango_healthcheck_{name}").touch(
mode=0o666, exist_ok=True
)
def dispose(self) -> None:
for disposable in self._to_dispose:

View File

@ -29,31 +29,55 @@ from .hedger import Hedger
# A hedger that hedges perp positions using a spot market.
#
class PerpToSpotHedger(Hedger):
def __init__(self, group: mango.Group, underlying_market: mango.PerpMarket,
hedging_market: mango.SpotMarket, market_operations: mango.MarketOperations,
max_price_slippage_factor: Decimal, max_hedge_chunk_quantity: Decimal,
target_balance: mango.TargetBalance, action_threshold: Decimal,
pause_threshold: int = 0) -> None:
def __init__(
self,
group: mango.Group,
underlying_market: mango.PerpMarket,
hedging_market: mango.SpotMarket,
market_operations: mango.MarketOperations,
max_price_slippage_factor: Decimal,
max_hedge_chunk_quantity: Decimal,
target_balance: mango.TargetBalance,
action_threshold: Decimal,
pause_threshold: int = 0,
) -> None:
super().__init__()
if (underlying_market.base != hedging_market.base) or (underlying_market.quote != hedging_market.quote):
if (underlying_market.base != hedging_market.base) or (
underlying_market.quote != hedging_market.quote
):
raise Exception(
f"Market {hedging_market.symbol} cannot be used to hedge market {underlying_market.symbol}.")
f"Market {hedging_market.symbol} cannot be used to hedge market {underlying_market.symbol}."
)
if not mango.Instrument.symbols_match(target_balance.symbol, hedging_market.base.symbol):
raise Exception(f"Cannot target {target_balance.symbol} when hedging on {hedging_market.symbol}")
if not mango.Instrument.symbols_match(
target_balance.symbol, hedging_market.base.symbol
):
raise Exception(
f"Cannot target {target_balance.symbol} when hedging on {hedging_market.symbol}"
)
self.underlying_market: mango.PerpMarket = underlying_market
self.hedging_market: mango.SpotMarket = hedging_market
self.market_operations: mango.MarketOperations = market_operations
self.buy_price_adjustment_factor: Decimal = Decimal("1") + max_price_slippage_factor
self.sell_price_adjustment_factor: Decimal = Decimal("1") - max_price_slippage_factor
self.buy_price_adjustment_factor: Decimal = (
Decimal("1") + max_price_slippage_factor
)
self.sell_price_adjustment_factor: Decimal = (
Decimal("1") - max_price_slippage_factor
)
self.max_hedge_chunk_quantity: Decimal = max_hedge_chunk_quantity
resolved_target: mango.InstrumentValue = target_balance.resolve(hedging_market.base, Decimal(0), Decimal(0))
self.target_balance: Decimal = self.hedging_market.lot_size_converter.round_base(resolved_target.value)
resolved_target: mango.InstrumentValue = target_balance.resolve(
hedging_market.base, Decimal(0), Decimal(0)
)
self.target_balance: Decimal = (
self.hedging_market.lot_size_converter.round_base(resolved_target.value)
)
self.action_threshold: Decimal = action_threshold
self.market_index: int = group.slot_by_perp_market_address(underlying_market.address).index
self.market_index: int = group.slot_by_perp_market_address(
underlying_market.address
).index
self.pause_threshold: int = pause_threshold
self.pause_counter: int = self.pause_threshold
@ -61,63 +85,100 @@ class PerpToSpotHedger(Hedger):
def pulse(self, context: mango.Context, model_state: mango.ModelState) -> None:
if self.pause_counter < self.pause_threshold:
self.pause_counter += 1
self._logger.debug(f"Pausing trades for {self.pause_threshold} pulses - this is pulse {self.pause_counter}")
self._logger.debug(
f"Pausing trades for {self.pause_threshold} pulses - this is pulse {self.pause_counter}"
)
return
try:
perp_account: typing.Optional[mango.PerpAccount] = model_state.account.perp_accounts_by_index[self.market_index]
perp_account: typing.Optional[
mango.PerpAccount
] = model_state.account.perp_accounts_by_index[self.market_index]
if perp_account is None:
raise Exception(
f"Could not find perp account at index {self.market_index} in account {model_state.account.address}.")
f"Could not find perp account at index {self.market_index} in account {model_state.account.address}."
)
basket_token: typing.Optional[mango.AccountSlot] = model_state.account.slots_by_index[self.market_index]
basket_token: typing.Optional[
mango.AccountSlot
] = model_state.account.slots_by_index[self.market_index]
if basket_token is None:
raise Exception(
f"Could not find basket token at index {self.market_index} in account {model_state.account.address}.")
f"Could not find basket token at index {self.market_index} in account {model_state.account.address}."
)
token_balance: mango.InstrumentValue = basket_token.net_value
perp_position: mango.InstrumentValue = perp_account.base_token_value
# We're interested in maintaining the right size of hedge lots, so round everything to the hedge
# market's lot size (even though perps have different lot sizes).
perp_position_rounded: Decimal = self.hedging_market.lot_size_converter.round_base(perp_position.value)
token_balance_rounded: Decimal = self.hedging_market.lot_size_converter.round_base(token_balance.value)
perp_position_rounded: Decimal = (
self.hedging_market.lot_size_converter.round_base(perp_position.value)
)
token_balance_rounded: Decimal = (
self.hedging_market.lot_size_converter.round_base(token_balance.value)
)
# When we add the rounded perp position and token balances, we should get zero if we're delta-neutral.
# If we have a target balance, subtract that to get our targetted delta neutral balance.
delta: Decimal = perp_position_rounded + token_balance_rounded - self.target_balance
delta: Decimal = (
perp_position_rounded + token_balance_rounded - self.target_balance
)
self._logger.debug(
f"Delta from {self.underlying_market.symbol} to {self.hedging_market.symbol} is {delta:,.8f} {basket_token.base_instrument.symbol}, action threshold is: {self.action_threshold}")
f"Delta from {self.underlying_market.symbol} to {self.hedging_market.symbol} is {delta:,.8f} {basket_token.base_instrument.symbol}, action threshold is: {self.action_threshold}"
)
if abs(delta) > self.action_threshold:
side: mango.Side = mango.Side.BUY if delta < 0 else mango.Side.SELL
up_or_down: str = "up to" if side == mango.Side.BUY else "down to"
price_adjustment_factor: Decimal = self.sell_price_adjustment_factor if side == mango.Side.SELL else self.buy_price_adjustment_factor
price_adjustment_factor: Decimal = (
self.sell_price_adjustment_factor
if side == mango.Side.SELL
else self.buy_price_adjustment_factor
)
adjusted_price: Decimal = model_state.price.mid_price * price_adjustment_factor
adjusted_price: Decimal = (
model_state.price.mid_price * price_adjustment_factor
)
quantity: Decimal = abs(delta)
if (self.max_hedge_chunk_quantity > 0) and (quantity > self.max_hedge_chunk_quantity):
if (self.max_hedge_chunk_quantity > 0) and (
quantity > self.max_hedge_chunk_quantity
):
self._logger.debug(
f"Quantity to hedge ({quantity:,.8f}) is bigger than maximum quantity to hedge in one chunk {self.max_hedge_chunk_quantity:,.8f} - reducing quantity to {self.max_hedge_chunk_quantity:,.8f}.")
f"Quantity to hedge ({quantity:,.8f}) is bigger than maximum quantity to hedge in one chunk {self.max_hedge_chunk_quantity:,.8f} - reducing quantity to {self.max_hedge_chunk_quantity:,.8f}."
)
quantity = self.max_hedge_chunk_quantity
order: mango.Order = mango.Order.from_basic_info(side, adjusted_price, quantity, mango.OrderType.IOC)
order: mango.Order = mango.Order.from_basic_info(
side, adjusted_price, quantity, mango.OrderType.IOC
)
self._logger.info(
f"Hedging perp position {perp_position} and token balance {token_balance} with {side} of {quantity:,.8f} at {up_or_down} ({model_state.price}) {adjusted_price:,.8f} on {self.hedging_market.symbol}\n\t{order}")
f"Hedging perp position {perp_position} and token balance {token_balance} with {side} of {quantity:,.8f} at {up_or_down} ({model_state.price}) {adjusted_price:,.8f} on {self.hedging_market.symbol}\n\t{order}"
)
try:
self.market_operations.place_order(order)
self.pause_counter = 0
except Exception:
self._logger.error(
f"[{context.name}] Failed to hedge on {self.hedging_market.symbol} using order {order} - {traceback.format_exc()}")
f"[{context.name}] Failed to hedge on {self.hedging_market.symbol} using order {order} - {traceback.format_exc()}"
)
raise
self.pulse_complete.on_next(datetime.now())
except (mango.RateLimitException, mango.NodeIsBehindException, mango.BlockhashNotFoundException, mango.FailedToFetchBlockhashException) as common_exception:
except (
mango.RateLimitException,
mango.NodeIsBehindException,
mango.BlockhashNotFoundException,
mango.FailedToFetchBlockhashException,
) as common_exception:
# Don't bother with a long traceback for these common problems.
self._logger.error(f"[{context.name}] Hedger problem on pulse: {common_exception}")
self._logger.error(
f"[{context.name}] Hedger problem on pulse: {common_exception}"
)
self.pulse_error.on_next(common_exception)
except Exception as exception:
self._logger.error(f"[{context.name}] Hedger error on pulse:\n{traceback.format_exc()}")
self._logger.error(
f"[{context.name}] Hedger error on pulse:\n{traceback.format_exc()}"
)
self.pulse_error.on_next(exception)
def __str__(self) -> str:

View File

@ -55,7 +55,9 @@ def _load_idl_parsers_from_json_file(filepath: str) -> typing.Dict[bytes, IdlTyp
sha = hashlib.sha256(f"event:{name}".encode())
return sha.digest()[0:8]
def _context_counter_lookup(field_counter: str) -> typing.Callable[[typing.Any], int]:
def _context_counter_lookup(
field_counter: str,
) -> typing.Callable[[typing.Any], int]:
return lambda ctx: int(ctx[field_counter])
with open(filepath, encoding="utf-8") as json_file:
@ -74,7 +76,12 @@ def _load_idl_parsers_from_json_file(filepath: str) -> typing.Dict[bytes, IdlTyp
counter_name: str = f"{field_name}_count"
fields += [counter_name / construct.BytesInteger(4, swapped=True)]
inner_loader = _known_idl_type_adapters[inner_type]
fields += [field_name / construct.Array(_context_counter_lookup(counter_name), inner_loader())]
fields += [
field_name
/ construct.Array(
_context_counter_lookup(counter_name), inner_loader()
)
]
else:
fields += [field_name / _known_idl_type_adapters[field_type]()]
layout_loaders[discriminator] = IdlType(event_name, construct.Struct(*fields))
@ -83,7 +90,9 @@ def _load_idl_parsers_from_json_file(filepath: str) -> typing.Dict[bytes, IdlTyp
class IdlParser:
def __init__(self, filepath: str):
self.parsers: typing.Dict[bytes, IdlType] = _load_idl_parsers_from_json_file(filepath)
self.parsers: typing.Dict[bytes, IdlType] = _load_idl_parsers_from_json_file(
filepath
)
def parse(self, binary_data: bytes) -> typing.Tuple[str, typing.Any]:
discriminator: bytes = binary_data[0:8]

View File

@ -51,21 +51,38 @@ class IdsJsonMarketLookup(MarketLookup):
self.instrument_lookup: InstrumentLookup = instrument_lookup
@staticmethod
def _from_dict(market_type: IdsJsonMarketType, mango_program_address: PublicKey, group_address: PublicKey, data: typing.Dict[str, typing.Any], instrument_lookup: InstrumentLookup, quote_symbol: str) -> Market:
def _from_dict(
market_type: IdsJsonMarketType,
mango_program_address: PublicKey,
group_address: PublicKey,
data: typing.Dict[str, typing.Any],
instrument_lookup: InstrumentLookup,
quote_symbol: str,
) -> Market:
base_symbol = data["baseSymbol"]
base_instrument: typing.Optional[Instrument] = instrument_lookup.find_by_symbol(base_symbol)
base_instrument: typing.Optional[Instrument] = instrument_lookup.find_by_symbol(
base_symbol
)
if base_instrument is None:
raise Exception(f"Could not find base instrument with symbol '{base_symbol}'")
quote_instrument: typing.Optional[Instrument] = instrument_lookup.find_by_symbol(quote_symbol)
raise Exception(
f"Could not find base instrument with symbol '{base_symbol}'"
)
quote_instrument: typing.Optional[
Instrument
] = instrument_lookup.find_by_symbol(quote_symbol)
if quote_instrument is None:
raise Exception(f"Could not find quote token with symbol '{quote_symbol}'")
quote: Token = Token.ensure(quote_instrument)
address = PublicKey(data["publicKey"])
if market_type == IdsJsonMarketType.PERP:
return PerpMarketStub(mango_program_address, address, base_instrument, quote, group_address)
return PerpMarketStub(
mango_program_address, address, base_instrument, quote, group_address
)
else:
base: Token = Token.ensure(base_instrument)
return SpotMarketStub(mango_program_address, address, base, quote, group_address)
return SpotMarketStub(
mango_program_address, address, base, quote, group_address
)
def find_by_symbol(self, symbol: str) -> typing.Optional[Market]:
check_spots = True
@ -73,10 +90,14 @@ class IdsJsonMarketLookup(MarketLookup):
symbol = symbol.upper()
if symbol.startswith("SPOT:"):
symbol = symbol.split(":", 1)[1]
check_perps = False # Skip perp markets because we're explicitly told it's a spot
check_perps = (
False # Skip perp markets because we're explicitly told it's a spot
)
elif symbol.startswith("PERP:"):
symbol = symbol.split(":", 1)[1]
check_spots = False # Skip spot markets because we're explicitly told it's a perp
check_spots = (
False # Skip spot markets because we're explicitly told it's a perp
)
for group in MangoConstants["groups"]:
if group["cluster"] == self.cluster_name:
@ -85,11 +106,25 @@ class IdsJsonMarketLookup(MarketLookup):
if check_perps:
for market_data in group["perpMarkets"]:
if Market.symbols_match(market_data["name"], symbol):
return IdsJsonMarketLookup._from_dict(IdsJsonMarketType.PERP, mango_program_address, group_address, market_data, self.instrument_lookup, group["quoteSymbol"])
return IdsJsonMarketLookup._from_dict(
IdsJsonMarketType.PERP,
mango_program_address,
group_address,
market_data,
self.instrument_lookup,
group["quoteSymbol"],
)
if check_spots:
for market_data in group["spotMarkets"]:
if Market.symbols_match(market_data["name"], symbol):
return IdsJsonMarketLookup._from_dict(IdsJsonMarketType.SPOT, mango_program_address, group_address, market_data, self.instrument_lookup, group["quoteSymbol"])
return IdsJsonMarketLookup._from_dict(
IdsJsonMarketType.SPOT,
mango_program_address,
group_address,
market_data,
self.instrument_lookup,
group["quoteSymbol"],
)
return None
def find_by_address(self, address: PublicKey) -> typing.Optional[Market]:
@ -99,10 +134,24 @@ class IdsJsonMarketLookup(MarketLookup):
mango_program_address: PublicKey = PublicKey(group["mangoProgramId"])
for market_data in group["perpMarkets"]:
if market_data["publicKey"] == str(address):
return IdsJsonMarketLookup._from_dict(IdsJsonMarketType.PERP, mango_program_address, group_address, market_data, self.instrument_lookup, group["quoteSymbol"])
return IdsJsonMarketLookup._from_dict(
IdsJsonMarketType.PERP,
mango_program_address,
group_address,
market_data,
self.instrument_lookup,
group["quoteSymbol"],
)
for market_data in group["spotMarkets"]:
if market_data["publicKey"] == str(address):
return IdsJsonMarketLookup._from_dict(IdsJsonMarketType.SPOT, mango_program_address, group_address, market_data, self.instrument_lookup, group["quoteSymbol"])
return IdsJsonMarketLookup._from_dict(
IdsJsonMarketType.SPOT,
mango_program_address,
group_address,
market_data,
self.instrument_lookup,
group["quoteSymbol"],
)
return None
def all_markets(self) -> typing.Sequence[Market]:
@ -113,11 +162,23 @@ class IdsJsonMarketLookup(MarketLookup):
mango_program_address: PublicKey = PublicKey(group["mangoProgramId"])
for market_data in group["perpMarkets"]:
market = IdsJsonMarketLookup._from_dict(
IdsJsonMarketType.PERP, mango_program_address, group_address, market_data, self.instrument_lookup, group["quoteSymbol"])
IdsJsonMarketType.PERP,
mango_program_address,
group_address,
market_data,
self.instrument_lookup,
group["quoteSymbol"],
)
markets = [market]
for market_data in group["spotMarkets"]:
market = IdsJsonMarketLookup._from_dict(
IdsJsonMarketType.SPOT, mango_program_address, group_address, market_data, self.instrument_lookup, group["quoteSymbol"])
IdsJsonMarketType.SPOT,
mango_program_address,
group_address,
market_data,
self.instrument_lookup,
group["quoteSymbol"],
)
markets = [market]
return markets

Some files were not shown because too many files have changed in this diff Show More