Switched from autopep8 to black for code formatting. Reformatted all files. Updated dependencies.
This commit is contained in:
parent
9fdccca3a3
commit
5c3b0befa9
2
.flake8
2
.flake8
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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()}"
|
||||
)
|
||||
|
|
42
bin/airdrop
42
bin/airdrop
|
@ -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)
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
39
bin/deposit
39
bin/deposit
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}.")
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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}
|
||||
==================================================================================""")
|
||||
=================================================================================="""
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.")
|
||||
|
|
268
bin/liquidator
268
bin/liquidator
|
@ -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.")
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
|
|
311
bin/marketmaker
311
bin/marketmaker
|
@ -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.
|
||||
|
|
33
bin/mint
33
bin/mint
|
@ -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"])
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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)
|
||||
|
|
114
bin/redeem-mango
114
bin/redeem-mango
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()}"
|
||||
)
|
||||
|
|
|
@ -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()}"
|
||||
)
|
||||
|
|
|
@ -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()}"
|
||||
)
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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()}"
|
||||
)
|
||||
|
|
|
@ -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()}"
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}")
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()}"
|
||||
)
|
||||
|
|
|
@ -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}"
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}.")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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()}"
|
||||
)
|
||||
|
|
|
@ -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()}"
|
||||
)
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
50
bin/withdraw
50
bin/withdraw
|
@ -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)
|
||||
|
|
49
bin/wrap-sol
49
bin/wrap-sol
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
533
mango/account.py
533
mango/account.py
|
@ -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}
|
||||
|
|
|
@ -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]] = []
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}.")
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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()}")
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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."
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)}")
|
||||
|
|
|
@ -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."
|
||||
)
|
||||
|
|
|
@ -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)}")
|
||||
|
|
|
@ -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)
|
||||
|
|
587
mango/client.py
587
mango/client.py
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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}"
|
||||
)
|
||||
|
|
|
@ -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")
|
||||
|
|
303
mango/group.py
303
mango/group.py
|
@ -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}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
15
mango/idl.py
15
mango/idl.py
|
@ -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]
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue