From 5c3b0befa94a9e25fb1045504746f93a989ed2da Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Wed, 9 Feb 2022 19:31:50 +0000 Subject: [PATCH] Switched from autopep8 to black for code formatting. Reformatted all files. Updated dependencies. --- .flake8 | 2 +- .vscode/settings.json | 8 +- bin/account-scout | 21 +- bin/airdrop | 42 +- bin/balance-account | 73 +- bin/balance-wallet | 62 +- bin/cancel-my-orders | 44 +- bin/cancel-order | 61 +- bin/close-wrapped-sol-account | 24 +- bin/crank-market | 37 +- bin/delegate-account | 46 +- bin/deposit | 39 +- bin/download-trades | 29 +- bin/ensure-account | 23 +- bin/ensure-associated-token-account | 31 +- bin/ensure-open-orders | 34 +- bin/generate-keypair | 34 +- bin/init-account | 10 +- bin/liquidate-single-account | 85 +- bin/liquidator | 268 ++++-- bin/liquidator-single-run | 86 +- bin/log-subscribe | 14 +- bin/mango-explorer-version | 3 +- bin/marketmaker | 311 +++++-- bin/mint | 33 +- bin/notify-below-minimum-sol-balance | 30 +- bin/place-order | 59 +- bin/redeem-mango | 114 ++- bin/register-referrer-id | 37 +- bin/report-transactions | 142 ++- bin/send-notification | 24 +- bin/send-sols | 34 +- bin/send-token | 74 +- bin/serum-buy | 47 +- bin/serum-sell | 47 +- bin/set-referrer | 29 +- bin/settle-market | 37 +- bin/show-account-balances | 111 ++- bin/show-account-dataframe | 32 +- bin/show-account-info | 19 +- bin/show-account-valuation | 21 +- bin/show-accounts | 11 +- bin/show-address | 30 +- bin/show-delegated-accounts | 11 +- bin/show-file | 27 +- bin/show-funding-rates | 13 +- bin/show-group | 15 +- bin/show-group-prices | 11 +- bin/show-health | 40 +- bin/show-interest-rates | 14 +- bin/show-liquidity-mining-info | 14 +- bin/show-market | 7 +- bin/show-model-state | 53 +- bin/show-my-orders | 34 +- bin/show-open-orders | 33 +- bin/show-orders | 26 +- bin/show-price | 40 +- bin/show-serum-open-orders | 26 +- bin/show-token-balance | 40 +- bin/show-transaction | 11 +- bin/show-transaction-logs | 10 +- bin/show-wrapped-sol | 15 +- bin/simple-marketmaker | 92 +- bin/transaction-scout | 22 +- bin/unwrap-sol | 42 +- bin/watch-address | 64 +- bin/watch-liquidations | 50 +- bin/watch-minimum-balances | 78 +- bin/who-am-i | 3 +- bin/withdraw | 50 +- bin/wrap-sol | 49 +- mango/__init__.py | 192 +++- mango/account.py | 533 ++++++++---- mango/accountflags.py | 29 +- mango/accountinfo.py | 41 +- mango/accountinfoconverter.py | 31 +- mango/accountinstrumentvalues.py | 257 ++++-- mango/accountliquidator.py | 30 +- mango/accountscout.py | 30 +- mango/arguments.py | 47 +- mango/balancesheet.py | 13 +- mango/cache.py | 98 ++- mango/calculators/collateralcalculator.py | 12 +- mango/calculators/healthcalculator.py | 144 ++- mango/calculators/perpcollateralcalculator.py | 26 +- .../calculators/serumcollateralcalculator.py | 12 +- mango/calculators/spotcollateralcalculator.py | 43 +- .../calculators/unsettledfundingcalculator.py | 8 +- mango/client.py | 587 ++++++++++--- mango/combinableinstructions.py | 128 ++- mango/constants.py | 18 +- mango/context.py | 76 +- mango/contextbuilder.py | 492 ++++++++--- mango/createmarketoperations.py | 70 +- mango/encoding.py | 2 +- mango/group.py | 303 +++++-- mango/healthcheck.py | 7 +- mango/hedging/perptospothedger.py | 127 ++- mango/idl.py | 15 +- mango/idsjsonmarketlookup.py | 89 +- mango/instructionreporter.py | 14 +- mango/instructions.py | 823 ++++++++++++++---- mango/instrumentlookup.py | 62 +- mango/instrumentvalue.py | 75 +- mango/inventory.py | 87 +- mango/layouts/layouts.py | 297 ++++--- mango/liquidatablereport.py | 22 +- mango/liquidationevent.py | 21 +- mango/liquidationprocessor.py | 111 ++- mango/loadedmarket.py | 42 +- mango/logmessages.py | 6 +- mango/lotsizeconverter.py | 63 +- mango/mangoinstruction.py | 7 +- mango/market.py | 18 +- mango/marketlookup.py | 21 +- mango/marketmaking/__init__.py | 24 +- mango/marketmaking/marketmaker.py | 99 ++- mango/marketmaking/modelstatebuilder.py | 328 ++++--- .../marketmaking/modelstatebuilderfactory.py | 270 ++++-- .../afteraccumulateddepthelement.py | 69 +- .../biasquantityonpositionelement.py | 85 +- .../orderchain/biasquoteelement.py | 35 +- .../orderchain/biasquoteonpositionelement.py | 35 +- mango/marketmaking/orderchain/chain.py | 4 +- mango/marketmaking/orderchain/chainbuilder.py | 11 +- .../orderchain/confidenceintervalelement.py | 86 +- mango/marketmaking/orderchain/element.py | 15 +- .../orderchain/fixedpositionsizeelement.py | 47 +- .../orderchain/fixedspreadelement.py | 37 +- .../orderchain/maximumquantityelement.py | 46 +- .../orderchain/minimumchargeelement.py | 64 +- .../orderchain/minimumquantityelement.py | 47 +- .../orderchain/pairwiseelement.py | 40 +- .../preventpostonlycrossingbookelement.py | 52 +- .../orderchain/quotesinglesideelement.py | 30 +- .../marketmaking/orderchain/ratioselement.py | 89 +- .../orderchain/roundtolotsizeelement.py | 39 +- .../orderchain/topofbookelement.py | 49 +- mango/marketmaking/orderreconciler.py | 25 +- .../marketmaking/toleranceorderreconciler.py | 21 +- mango/marketoperations.py | 71 +- mango/metadata.py | 6 +- mango/modelstate.py | 30 +- mango/notification.py | 64 +- mango/observables.py | 37 +- mango/openorders.py | 144 ++- mango/oracle.py | 47 +- mango/oraclefactory.py | 4 +- mango/oracles/ftx/ftx.py | 46 +- mango/oracles/market/market.py | 38 +- mango/oracles/pythnetwork/layouts.py | 44 +- mango/oracles/pythnetwork/pythnetwork.py | 72 +- mango/oracles/stub/stub.py | 39 +- mango/orderbookside.py | 92 +- mango/orders.py | 145 ++- mango/output.py | 62 +- mango/ownedinstrumentvalue.py | 16 +- mango/parse_account_info_to_orders.py | 12 +- mango/perpaccount.py | 115 ++- mango/perpeventqueue.py | 228 +++-- mango/perpmarket.py | 142 ++- mango/perpmarketdetails.py | 125 ++- mango/perpmarketoperations.py | 173 +++- mango/placedorder.py | 11 +- mango/publickey.py | 2 +- mango/reconnectingwebsocket.py | 28 +- mango/retrier.py | 41 +- mango/serumeventqueue.py | 117 ++- mango/serummarket.py | 85 +- mango/serummarketlookup.py | 142 ++- mango/serummarketoperations.py | 246 ++++-- mango/simplemarketmaking/simplemarketmaker.py | 114 ++- mango/spotmarket.py | 89 +- mango/spotmarketoperations.py | 240 +++-- mango/token.py | 20 +- mango/tokenaccount.py | 115 ++- mango/tokenbank.py | 114 ++- mango/tradeexecutor.py | 30 +- mango/tradehistory.py | 161 +++- mango/transactionscout.py | 151 +++- mango/valuation.py | 132 ++- mango/wallet.py | 16 +- mango/walletbalancer.py | 139 ++- mango/watchers.py | 331 +++++-- mango/websocketsubscription.py | 134 ++- poetry.lock | 470 ++++------ pyproject.toml | 6 +- scripts/run-jupyter | 17 - tests/calculations/test_healthcalculator.py | 24 +- tests/context.py | 4 +- tests/data.py | 40 +- tests/fakes.py | 394 +++++++-- tests/layouts/test_layouts.py | 36 +- .../test_afteraccumulateddepthelement.py | 91 +- .../test_biasquantityonpositionelement.py | 175 +++- .../orderchain/test_biasquoteelement.py | 20 +- .../test_biasquoteonpositionelement.py | 99 ++- .../test_fixedpositionsizeelement.py | 24 +- .../orderchain/test_fixedspreadelement.py | 12 +- .../test_maximumpositionsizeelement.py | 40 +- .../orderchain/test_minimumchargeelement.py | 36 +- .../test_minimumpositionsizeelement.py | 40 +- ...test_preventpostonlycrossingbookelement.py | 72 +- tests/marketmaking/orderchain/test_ratios.py | 28 +- .../orderchain/test_roundtolotsizeelement.py | 12 +- .../orderchain/test_singlesidedelement.py | 10 +- .../orderchain/test_topofbookelement.py | 64 +- tests/marketmaking/test_orderreconciler.py | 37 +- .../test_toleranceorderreconciler.py | 208 +++-- tests/test_account.py | 231 ++++- tests/test_accountflags.py | 27 +- tests/test_cache.py | 133 ++- tests/test_client.py | 30 +- tests/test_context.py | 24 +- tests/test_group.py | 188 +++- tests/test_healthcalculator.py | 172 +++- tests/test_instructions.py | 48 +- tests/test_instrumentlookup.py | 15 +- tests/test_liquidationevent.py | 23 +- tests/test_logmessages.py | 50 +- tests/test_lotsizeconverter.py | 8 +- tests/test_marketlookup.py | 81 +- tests/test_notification.py | 14 +- tests/test_openorders.py | 20 +- tests/test_orderbook.py | 11 +- tests/test_perpeventqueue.py | 176 +++- tests/test_publickey.py | 22 +- tests/test_spotmarket.py | 8 +- tests/test_tokenaccount.py | 4 +- tests/test_tokenbank.py | 46 +- tests/test_tradeexecutor.py | 6 +- tests/test_transactionscout.py | 21 +- tests/test_walletbalancer.py | 104 ++- 233 files changed, 13052 insertions(+), 4563 deletions(-) delete mode 100755 scripts/run-jupyter diff --git a/.flake8 b/.flake8 index 645f497..a0985b2 100644 --- a/.flake8 +++ b/.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 diff --git a/.vscode/settings.json b/.vscode/settings.json index 3742b6b..a0aacad 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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" } \ No newline at end of file diff --git a/bin/account-scout b/bin/account-scout index 010cbca..94dc608 100755 --- a/bin/account-scout +++ b/bin/account-scout @@ -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()}" + ) diff --git a/bin/airdrop b/bin/airdrop index 3077267..7dcf7fe 100755 --- a/bin/airdrop +++ b/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) diff --git a/bin/balance-account b/bin/balance-account index c95928d..291edf6 100755 --- a/bin/balance-account +++ b/bin/balance-account @@ -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.") diff --git a/bin/balance-wallet b/bin/balance-wallet index 7b387d0..03960dd 100755 --- a/bin/balance-wallet +++ b/bin/balance-wallet @@ -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.") diff --git a/bin/cancel-my-orders b/bin/cancel-my-orders index b113e97..da0ab55 100755 --- a/bin/cancel-my-orders +++ b/bin/cancel-my-orders @@ -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: diff --git a/bin/cancel-order b/bin/cancel-order index 89af104..ba00bbe 100755 --- a/bin/cancel-order +++ b/bin/cancel-order @@ -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) diff --git a/bin/close-wrapped-sol-account b/bin/close-wrapped-sol-account index 1abe7d2..ca50bd8 100755 --- a/bin/close-wrapped-sol-account +++ b/bin/close-wrapped-sol-account @@ -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) diff --git a/bin/crank-market b/bin/crank-market index 13dfcdb..993a4ce 100755 --- a/bin/crank-market +++ b/bin/crank-market @@ -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) diff --git a/bin/delegate-account b/bin/delegate-account index 8f5b1a4..c3bc829 100755 --- a/bin/delegate-account +++ b/bin/delegate-account @@ -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) diff --git a/bin/deposit b/bin/deposit index 7089581..bd25a9f 100755 --- a/bin/deposit +++ b/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) diff --git a/bin/download-trades b/bin/download-trades index 2d266d7..4bd55ee 100755 --- a/bin/download-trades +++ b/bin/download-trades @@ -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) diff --git a/bin/ensure-account b/bin/ensure-account index 7c1b432..730919b 100755 --- a/bin/ensure-account +++ b/bin/ensure-account @@ -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) diff --git a/bin/ensure-associated-token-account b/bin/ensure-associated-token-account index 6bd7d05..dec9ae0 100755 --- a/bin/ensure-associated-token-account +++ b/bin/ensure-associated-token-account @@ -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}.") diff --git a/bin/ensure-open-orders b/bin/ensure-open-orders index 005aacf..f1d1749 100755 --- a/bin/ensure-open-orders +++ b/bin/ensure-open-orders @@ -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}") diff --git a/bin/generate-keypair b/bin/generate-keypair index ebfdec2..354a32b 100755 --- a/bin/generate-keypair +++ b/bin/generate-keypair @@ -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} -==================================================================================""") +==================================================================================""" + ) diff --git a/bin/init-account b/bin/init-account index 3fee691..5284d7a 100755 --- a/bin/init-account +++ b/bin/init-account @@ -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) diff --git a/bin/liquidate-single-account b/bin/liquidate-single-account index 488ba84..f3452e6 100755 --- a/bin/liquidate-single-account +++ b/bin/liquidate-single-account @@ -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.") diff --git a/bin/liquidator b/bin/liquidator index 10685fb..fb96777 100755 --- a/bin/liquidator +++ b/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.") diff --git a/bin/liquidator-single-run b/bin/liquidator-single-run index 04f6928..0170033 100755 --- a/bin/liquidator-single-run +++ b/bin/liquidator-single-run @@ -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.") diff --git a/bin/log-subscribe b/bin/log-subscribe index 53e07c7..0642cc2 100755 --- a/bin/log-subscribe +++ b/bin/log-subscribe @@ -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) diff --git a/bin/mango-explorer-version b/bin/mango-explorer-version index fbe6983..1fa7206 100755 --- a/bin/mango-explorer-version +++ b/bin/mango-explorer-version @@ -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()) diff --git a/bin/marketmaker b/bin/marketmaker index 124f758..966d7fd 100755 --- a/bin/marketmaker +++ b/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. diff --git a/bin/mint b/bin/mint index 0162d8b..5d0abf1 100755 --- a/bin/mint +++ b/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"]) diff --git a/bin/notify-below-minimum-sol-balance b/bin/notify-below-minimum-sol-balance index ccf18b8..301bc67 100755 --- a/bin/notify-below-minimum-sol-balance +++ b/bin/notify-below-minimum-sol-balance @@ -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}") diff --git a/bin/place-order b/bin/place-order index 4bcc918..dd7f0d9 100755 --- a/bin/place-order +++ b/bin/place-order @@ -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) diff --git a/bin/redeem-mango b/bin/redeem-mango index b50d9c6..b1f78f3 100755 --- a/bin/redeem-mango +++ b/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: diff --git a/bin/register-referrer-id b/bin/register-referrer-id index d6bc5ed..0d0b016 100755 --- a/bin/register-referrer-id +++ b/bin/register-referrer-id @@ -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) diff --git a/bin/report-transactions b/bin/report-transactions index e67eb36..fb6bdfe 100755 --- a/bin/report-transactions +++ b/bin/report-transactions @@ -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()}" + ) diff --git a/bin/send-notification b/bin/send-notification index 8716872..f06ff87 100755 --- a/bin/send-notification +++ b/bin/send-notification @@ -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()}" + ) diff --git a/bin/send-sols b/bin/send-sols index acc9f3f..7d9a001 100755 --- a/bin/send-sols +++ b/bin/send-sols @@ -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()}" + ) diff --git a/bin/send-token b/bin/send-token index c093d04..2648e2e 100755 --- a/bin/send-token +++ b/bin/send-token @@ -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.") diff --git a/bin/serum-buy b/bin/serum-buy index 649f505..30bf651 100755 --- a/bin/serum-buy +++ b/bin/serum-buy @@ -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()}" + ) diff --git a/bin/serum-sell b/bin/serum-sell index d570503..b1a9a7d 100755 --- a/bin/serum-sell +++ b/bin/serum-sell @@ -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()}" + ) diff --git a/bin/set-referrer b/bin/set-referrer index ecae090..543e57c 100755 --- a/bin/set-referrer +++ b/bin/set-referrer @@ -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) diff --git a/bin/settle-market b/bin/settle-market index 9ed1126..09ca9c4 100755 --- a/bin/settle-market +++ b/bin/settle-market @@ -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) diff --git a/bin/show-account-balances b/bin/show-account-balances index e9d6922..6698ce6 100755 --- a/bin/show-account-balances +++ b/bin/show-account-balances @@ -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) diff --git a/bin/show-account-dataframe b/bin/show-account-dataframe index 48d161d..c7c1fcb 100755 --- a/bin/show-account-dataframe +++ b/bin/show-account-dataframe @@ -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) diff --git a/bin/show-account-info b/bin/show-account-info index f2ef3f9..5cdba26 100755 --- a/bin/show-account-info +++ b/bin/show-account-info @@ -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) diff --git a/bin/show-account-valuation b/bin/show-account-valuation index 82cda94..beccb15 100755 --- a/bin/show-account-valuation +++ b/bin/show-account-valuation @@ -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 diff --git a/bin/show-accounts b/bin/show-accounts index a8d8361..69541ba 100755 --- a/bin/show-accounts +++ b/bin/show-accounts @@ -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) diff --git a/bin/show-address b/bin/show-address index 85544f9..60f7407 100755 --- a/bin/show-address +++ b/bin/show-address @@ -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}") diff --git a/bin/show-delegated-accounts b/bin/show-delegated-accounts index 6a83561..ca64e50 100755 --- a/bin/show-delegated-accounts +++ b/bin/show-delegated-accounts @@ -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) diff --git a/bin/show-file b/bin/show-file index e01d8b4..1390fdc 100755 --- a/bin/show-file +++ b/bin/show-file @@ -6,20 +6,31 @@ import os.path import sys import typing -sys.path.insert(0, os.path.abspath( - os.path.join(os.path.dirname(__file__), ".."))) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) import mango # nopep8 -parser = argparse.ArgumentParser(description="Shows the on-chain data of a particular account.") +parser = argparse.ArgumentParser( + description="Shows the on-chain data of a particular account." +) mango.ContextBuilder.add_command_line_parameters(parser) -parser.add_argument("--filename", type=str, required=False, - help="filename for loading the JSON-formatted AccountInfo data") -parser.add_argument("--account-type", type=str, default="AccountInfo", - help="Underlying object type of the data in the AccountInfo") +parser.add_argument( + "--filename", + type=str, + required=False, + help="filename for loading the JSON-formatted AccountInfo data", +) +parser.add_argument( + "--account-type", + type=str, + default="AccountInfo", + help="Underlying object type of the data in the AccountInfo", +) args: argparse.Namespace = mango.parse_args(parser) context = mango.ContextBuilder.from_command_line_parameters(args) -converter: typing.Callable[[mango.AccountInfo], typing.Any] = lambda account_info: account_info +converter: typing.Callable[ + [mango.AccountInfo], typing.Any +] = lambda account_info: account_info if args.account_type.upper() != "ACCOUNTINFO": converter = mango.build_account_info_converter(context, args.account_type) diff --git a/bin/show-funding-rates b/bin/show-funding-rates index e1af7a3..00ebc3f 100755 --- a/bin/show-funding-rates +++ b/bin/show-funding-rates @@ -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) diff --git a/bin/show-group b/bin/show-group index 35a1459..d197eac 100755 --- a/bin/show-group +++ b/bin/show-group @@ -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()}" + ) diff --git a/bin/show-group-prices b/bin/show-group-prices index e7fb2ce..4b0b9bf 100755 --- a/bin/show-group-prices +++ b/bin/show-group-prices @@ -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}" + ) diff --git a/bin/show-health b/bin/show-health index 67f8b62..8e11525 100755 --- a/bin/show-health +++ b/bin/show-health @@ -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) +) diff --git a/bin/show-interest-rates b/bin/show-interest-rates index 0567cde..f7f2c23 100755 --- a/bin/show-interest-rates +++ b/bin/show-interest-rates @@ -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) diff --git a/bin/show-liquidity-mining-info b/bin/show-liquidity-mining-info index 536ceb5..e632733 100755 --- a/bin/show-liquidity-mining-info +++ b/bin/show-liquidity-mining-info @@ -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) diff --git a/bin/show-market b/bin/show-market index b60d3df..b2710f5 100755 --- a/bin/show-market +++ b/bin/show-market @@ -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) diff --git a/bin/show-model-state b/bin/show-model-state index f63088e..d14869e 100755 --- a/bin/show-model-state +++ b/bin/show-model-state @@ -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) diff --git a/bin/show-my-orders b/bin/show-my-orders index 19e09dc..87cc927 100755 --- a/bin/show-my-orders +++ b/bin/show-my-orders @@ -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: diff --git a/bin/show-open-orders b/bin/show-open-orders index fb47e71..bbe6f96 100755 --- a/bin/show-open-orders +++ b/bin/show-open-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 diff --git a/bin/show-orders b/bin/show-orders index 38d1c22..68c43ad 100755 --- a/bin/show-orders +++ b/bin/show-orders @@ -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) diff --git a/bin/show-price b/bin/show-price index 03191fb..f1df0e6 100755 --- a/bin/show-price +++ b/bin/show-price @@ -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 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() diff --git a/bin/show-serum-open-orders b/bin/show-serum-open-orders index 1e87885..0c5aec1 100755 --- a/bin/show-serum-open-orders +++ b/bin/show-serum-open-orders @@ -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) diff --git a/bin/show-token-balance b/bin/show-token-balance index 03c0ba7..15419c2 100755 --- a/bin/show-token-balance +++ b/bin/show-token-balance @@ -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}.") diff --git a/bin/show-transaction b/bin/show-transaction index 45d2540..4bb9a0e 100755 --- a/bin/show-transaction +++ b/bin/show-transaction @@ -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) diff --git a/bin/show-transaction-logs b/bin/show-transaction-logs index 4626ee3..d35ca42 100755 --- a/bin/show-transaction-logs +++ b/bin/show-transaction-logs @@ -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) diff --git a/bin/show-wrapped-sol b/bin/show-wrapped-sol index 15553f4..62233d2 100755 --- a/bin/show-wrapped-sol +++ b/bin/show-wrapped-sol @@ -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.") diff --git a/bin/simple-marketmaker b/bin/simple-marketmaker index 04a0a51..6e4ce8b 100755 --- a/bin/simple-marketmaker +++ b/bin/simple-marketmaker @@ -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 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()}" + ) diff --git a/bin/transaction-scout b/bin/transaction-scout index 49f62ca..88f92c9 100755 --- a/bin/transaction-scout +++ b/bin/transaction-scout @@ -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()}" + ) diff --git a/bin/unwrap-sol b/bin/unwrap-sol index fef482d..54eead0 100755 --- a/bin/unwrap-sol +++ b/bin/unwrap-sol @@ -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}") diff --git a/bin/watch-address b/bin/watch-address index aa558e7..30ded4c 100755 --- a/bin/watch-address +++ b/bin/watch-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 diff --git a/bin/watch-liquidations b/bin/watch-liquidations index a9f1a67..415438d 100755 --- a/bin/watch-liquidations +++ b/bin/watch-liquidations @@ -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() diff --git a/bin/watch-minimum-balances b/bin/watch-minimum-balances index 1d91c08..12bd0f7 100755 --- a/bin/watch-minimum-balances +++ b/bin/watch-minimum-balances @@ -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() diff --git a/bin/who-am-i b/bin/who-am-i index 9a9a2ae..fc5e6a6 100755 --- a/bin/who-am-i +++ b/bin/who-am-i @@ -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") diff --git a/bin/withdraw b/bin/withdraw index 4d64435..d6406ae 100755 --- a/bin/withdraw +++ b/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) diff --git a/bin/wrap-sol b/bin/wrap-sol index 904e315..5ab5760 100755 --- a/bin/wrap-sol +++ b/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) diff --git a/mango/__init__.py b/mango/__init__.py index 9751c96..521fc18 100644 --- a/mango/__init__.py +++ b/mango/__init__.py @@ -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 diff --git a/mango/account.py b/mango/account.py index b29d668..0bfce50 100644 --- a/mango/account.py +++ b/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} diff --git a/mango/accountflags.py b/mango/accountflags.py index 167e230..018ab70 100644 --- a/mango/accountflags.py +++ b/mango/accountflags.py @@ -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]] = [] diff --git a/mango/accountinfo.py b/mango/accountinfo.py index df49e0c..749ef37 100644 --- a/mango/accountinfo.py +++ b/mango/accountinfo.py @@ -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 diff --git a/mango/accountinfoconverter.py b/mango/accountinfoconverter.py index d5ee449..70a288f 100644 --- a/mango/accountinfoconverter.py +++ b/mango/accountinfoconverter.py @@ -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}.") diff --git a/mango/accountinstrumentvalues.py b/mango/accountinstrumentvalues.py index cc2c753..e90384f 100644 --- a/mango/accountinstrumentvalues.py +++ b/mango/accountinstrumentvalues.py @@ -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 diff --git a/mango/accountliquidator.py b/mango/accountliquidator.py index 1b43192..1e29507 100644 --- a/mango/accountliquidator.py +++ b/mango/accountliquidator.py @@ -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 diff --git a/mango/accountscout.py b/mango/accountscout.py index 928bf24..d5aeb17 100644 --- a/mango/accountscout.py +++ b/mango/accountscout.py @@ -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}") diff --git a/mango/arguments.py b/mango/arguments.py index d192484..8f8256d 100644 --- a/mango/arguments.py +++ b/mango/arguments.py @@ -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()}") diff --git a/mango/balancesheet.py b/mango/balancesheet.py index 435accf..dec3380 100644 --- a/mango/balancesheet.py +++ b/mango/balancesheet.py @@ -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)) diff --git a/mango/cache.py b/mango/cache.py index e86070b..6db7375 100644 --- a/mango/cache.py +++ b/mango/cache.py @@ -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: diff --git a/mango/calculators/collateralcalculator.py b/mango/calculators/collateralcalculator.py index 0dbdf28..ba1df21 100644 --- a/mango/calculators/collateralcalculator.py +++ b/mango/calculators/collateralcalculator.py @@ -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." + ) diff --git a/mango/calculators/healthcalculator.py b/mango/calculators/healthcalculator.py index ccf28c9..c136d21 100644 --- a/mango/calculators/healthcalculator.py +++ b/mango/calculators/healthcalculator.py @@ -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 diff --git a/mango/calculators/perpcollateralcalculator.py b/mango/calculators/perpcollateralcalculator.py index d6ff8f7..cc72eff 100644 --- a/mango/calculators/perpcollateralcalculator.py +++ b/mango/calculators/perpcollateralcalculator.py @@ -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)}") diff --git a/mango/calculators/serumcollateralcalculator.py b/mango/calculators/serumcollateralcalculator.py index e33683e..13af626 100644 --- a/mango/calculators/serumcollateralcalculator.py +++ b/mango/calculators/serumcollateralcalculator.py @@ -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." + ) diff --git a/mango/calculators/spotcollateralcalculator.py b/mango/calculators/spotcollateralcalculator.py index df8d07b..ce57032 100644 --- a/mango/calculators/spotcollateralcalculator.py +++ b/mango/calculators/spotcollateralcalculator.py @@ -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)}") diff --git a/mango/calculators/unsettledfundingcalculator.py b/mango/calculators/unsettledfundingcalculator.py index 710971d..b555e1d 100644 --- a/mango/calculators/unsettledfundingcalculator.py +++ b/mango/calculators/unsettledfundingcalculator.py @@ -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) diff --git a/mango/client.py b/mango/client.py index 6af5b84..f84355a 100644 --- a/mango/client.py +++ b/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 diff --git a/mango/combinableinstructions.py b/mango/combinableinstructions.py index e928636..3ec90ac 100644 --- a/mango/combinableinstructions.py +++ b/mango/combinableinstructions.py @@ -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: diff --git a/mango/constants.py b/mango/constants.py index fea39d0..cac7973 100644 --- a/mango/constants.py +++ b/mango/constants.py @@ -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 diff --git a/mango/context.py b/mango/context.py index 5f08ef6..31280ae 100644 --- a/mango/context.py +++ b/mango/context.py @@ -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: diff --git a/mango/contextbuilder.py b/mango/contextbuilder.py index 6baead3..44cd1d6 100644 --- a/mango/contextbuilder.py +++ b/mango/contextbuilder.py @@ -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, + ) diff --git a/mango/createmarketoperations.py b/mango/createmarketoperations.py index 7aa213f..7e55b8f 100644 --- a/mango/createmarketoperations.py +++ b/mango/createmarketoperations.py @@ -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}" + ) diff --git a/mango/encoding.py b/mango/encoding.py index bf972c6..036c6d5 100644 --- a/mango/encoding.py +++ b/mango/encoding.py @@ -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") diff --git a/mango/group.py b/mango/group.py index 521c7da..95c23ae 100644 --- a/mango/group.py +++ b/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} diff --git a/mango/healthcheck.py b/mango/healthcheck.py index 30c6572..5ec594e 100644 --- a/mango/healthcheck.py +++ b/mango/healthcheck.py @@ -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: diff --git a/mango/hedging/perptospothedger.py b/mango/hedging/perptospothedger.py index ed89631..89fc4b7 100644 --- a/mango/hedging/perptospothedger.py +++ b/mango/hedging/perptospothedger.py @@ -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: diff --git a/mango/idl.py b/mango/idl.py index 3777eff..a2e7859 100644 --- a/mango/idl.py +++ b/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] diff --git a/mango/idsjsonmarketlookup.py b/mango/idsjsonmarketlookup.py index 9928c20..09bc3aa 100644 --- a/mango/idsjsonmarketlookup.py +++ b/mango/idsjsonmarketlookup.py @@ -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 diff --git a/mango/instructionreporter.py b/mango/instructionreporter.py index 3a6bdf2..1053608 100644 --- a/mango/instructionreporter.py +++ b/mango/instructionreporter.py @@ -98,9 +98,12 @@ class MangoInstructionReporter(InstructionReporter): parser = layouts.InstructionParsersByVariant[initial.variant] if parser is None: raise Exception( - f"Could not find instruction parser for variant {initial.variant} / {InstructionType(initial.variant)}.") + f"Could not find instruction parser for variant {initial.variant} / {InstructionType(initial.variant)}." + ) - accounts: typing.List[PublicKey] = list(map(lambda meta: meta.pubkey, instruction.keys)) + accounts: typing.List[PublicKey] = list( + map(lambda meta: meta.pubkey, instruction.keys) + ) parsed = parser.parse(instruction.data) instruction_type = InstructionType(int(parsed.variant)) @@ -139,10 +142,13 @@ class CompoundInstructionReporter(InstructionReporter): if reporter.matches(instruction): return reporter.report(instruction) raise Exception( - f"Could not find instruction reporter for instruction {instruction}.") + f"Could not find instruction reporter for instruction {instruction}." + ) @staticmethod - def from_addresses(mango_program_address: PublicKey, serum_program_address: PublicKey) -> InstructionReporter: + def from_addresses( + mango_program_address: PublicKey, serum_program_address: PublicKey + ) -> InstructionReporter: base: InstructionReporter = InstructionReporter() serum: InstructionReporter = SerumInstructionReporter(serum_program_address) mango: InstructionReporter = MangoInstructionReporter(mango_program_address) diff --git a/mango/instructions.py b/mango/instructions.py index 3acdd53..239d309 100644 --- a/mango/instructions.py +++ b/mango/instructions.py @@ -18,17 +18,33 @@ import pyserum.enums import typing from decimal import Decimal -from pyserum._layouts.instructions import INSTRUCTIONS_LAYOUT as PYSERUM_INSTRUCTIONS_LAYOUT, InstructionType as PySerumInstructionType +from pyserum._layouts.instructions import ( + INSTRUCTIONS_LAYOUT as PYSERUM_INSTRUCTIONS_LAYOUT, + InstructionType as PySerumInstructionType, +) from pyserum.enums import OrderType as PySerumOrderType, Side as PySerumSide -from pyserum.instructions import settle_funds as pyserum_settle_funds, SettleFundsParams as PySerumSettleFundsParams +from pyserum.instructions import ( + settle_funds as pyserum_settle_funds, + SettleFundsParams as PySerumSettleFundsParams, +) from pyserum.market.market import Market as PySerumMarket -from pyserum.open_orders_account import make_create_account_instruction as pyserum_make_create_account_instruction +from pyserum.open_orders_account import ( + make_create_account_instruction as pyserum_make_create_account_instruction, +) from solana.keypair import Keypair from solana.publickey import PublicKey from solana.system_program import CreateAccountParams, create_account from solana.transaction import AccountMeta, TransactionInstruction from spl.token.constants import ACCOUNT_LEN, TOKEN_PROGRAM_ID -from spl.token.instructions import CloseAccountParams, InitializeAccountParams, TransferParams, close_account, create_associated_token_account, initialize_account, transfer +from spl.token.instructions import ( + CloseAccountParams, + InitializeAccountParams, + TransferParams, + close_account, + create_associated_token_account, + initialize_account, + transfer, +) from .account import Account from .combinableinstructions import CombinableInstructions @@ -64,11 +80,24 @@ from .wallet import Wallet # Creates and initializes an SPL token account. Can add additional lamports too but that's usually not # necesary. # -def build_create_solana_account_instructions(context: Context, wallet: Wallet, mango_program_address: PublicKey, size: int, lamports: int = 0) -> CombinableInstructions: +def build_create_solana_account_instructions( + context: Context, + wallet: Wallet, + mango_program_address: PublicKey, + size: int, + lamports: int = 0, +) -> CombinableInstructions: minimum_balance = context.client.get_minimum_balance_for_rent_exemption(size) account = Keypair() create_instruction = create_account( - CreateAccountParams(wallet.address, account.public_key, lamports + minimum_balance, size, mango_program_address)) + CreateAccountParams( + wallet.address, + account.public_key, + lamports + minimum_balance, + size, + mango_program_address, + ) + ) return CombinableInstructions(signers=[account], instructions=[create_instruction]) @@ -80,12 +109,23 @@ def build_create_solana_account_instructions(context: Context, wallet: Wallet, m # Prefer `build_create_spl_account_instructions()` over this function. This function should be # reserved for cases where you specifically don't want the associated token account. # -def build_create_spl_account_instructions(context: Context, wallet: Wallet, token: Token, lamports: int = 0) -> CombinableInstructions: +def build_create_spl_account_instructions( + context: Context, wallet: Wallet, token: Token, lamports: int = 0 +) -> CombinableInstructions: create_account_instructions = build_create_solana_account_instructions( - context, wallet, TOKEN_PROGRAM_ID, ACCOUNT_LEN, lamports) - initialize_instruction = initialize_account(InitializeAccountParams( - TOKEN_PROGRAM_ID, create_account_instructions.signers[0].public_key, token.mint, wallet.address)) - return create_account_instructions + CombinableInstructions(signers=[], instructions=[initialize_instruction]) + context, wallet, TOKEN_PROGRAM_ID, ACCOUNT_LEN, lamports + ) + initialize_instruction = initialize_account( + InitializeAccountParams( + TOKEN_PROGRAM_ID, + create_account_instructions.signers[0].public_key, + token.mint, + wallet.address, + ) + ) + return create_account_instructions + CombinableInstructions( + signers=[], instructions=[initialize_instruction] + ) # # ๐Ÿฅญ build_create_associated_spl_account_instructions function @@ -94,18 +134,37 @@ def build_create_spl_account_instructions(context: Context, wallet: Wallet, toke # token account now. `build_create_spl_account_instructions()` should be reserved for cases where # you specifically don't want the associated token account. # -def build_create_associated_spl_account_instructions(context: Context, wallet: Wallet, token: Token) -> CombinableInstructions: - create_account_instructions = create_associated_token_account(wallet.address, wallet.address, token.mint) - return CombinableInstructions(signers=[], instructions=[create_account_instructions]) +def build_create_associated_spl_account_instructions( + context: Context, wallet: Wallet, token: Token +) -> CombinableInstructions: + create_account_instructions = create_associated_token_account( + wallet.address, wallet.address, token.mint + ) + return CombinableInstructions( + signers=[], instructions=[create_account_instructions] + ) # # ๐Ÿฅญ build_transfer_spl_tokens_instructions function # # Creates an instruction to transfer SPL tokens from one account to another. # -def build_transfer_spl_tokens_instructions(context: Context, wallet: Wallet, token: Token, source: PublicKey, destination: PublicKey, quantity: Decimal) -> CombinableInstructions: - amount = int(quantity * (10 ** token.decimals)) - instructions = [transfer(TransferParams(TOKEN_PROGRAM_ID, source, destination, wallet.address, amount, []))] +def build_transfer_spl_tokens_instructions( + context: Context, + wallet: Wallet, + token: Token, + source: PublicKey, + destination: PublicKey, + quantity: Decimal, +) -> CombinableInstructions: + amount = int(quantity * (10**token.decimals)) + instructions = [ + transfer( + TransferParams( + TOKEN_PROGRAM_ID, source, destination, wallet.address, amount, [] + ) + ) + ] return CombinableInstructions(signers=[], instructions=instructions) @@ -113,17 +172,32 @@ def build_transfer_spl_tokens_instructions(context: Context, wallet: Wallet, tok # # Creates an instructio to close an SPL token account and transfers any remaining lamports to the wallet. # -def build_close_spl_account_instructions(context: Context, wallet: Wallet, address: PublicKey) -> CombinableInstructions: - return CombinableInstructions(signers=[], instructions=[close_account(CloseAccountParams(TOKEN_PROGRAM_ID, address, wallet.address, wallet.address))]) +def build_close_spl_account_instructions( + context: Context, wallet: Wallet, address: PublicKey +) -> CombinableInstructions: + return CombinableInstructions( + signers=[], + instructions=[ + close_account( + CloseAccountParams( + TOKEN_PROGRAM_ID, address, wallet.address, wallet.address + ) + ) + ], + ) # # ๐Ÿฅญ build_create_serum_open_orders_instructions function # # Creates a Serum openorders-creating instruction. # -def build_create_serum_open_orders_instructions(context: Context, wallet: Wallet, market: PySerumMarket) -> CombinableInstructions: +def build_create_serum_open_orders_instructions( + context: Context, wallet: Wallet, market: PySerumMarket +) -> CombinableInstructions: new_open_orders_account = Keypair() - minimum_balance = context.client.get_minimum_balance_for_rent_exemption(layouts.OPEN_ORDERS.sizeof()) + minimum_balance = context.client.get_minimum_balance_for_rent_exemption( + layouts.OPEN_ORDERS.sizeof() + ) instruction = pyserum_make_create_account_instruction( owner_address=wallet.address, new_account_address=new_open_orders_account.public_key, @@ -131,15 +205,35 @@ def build_create_serum_open_orders_instructions(context: Context, wallet: Wallet program_id=market.state.program_id(), ) - return CombinableInstructions(signers=[new_open_orders_account], instructions=[instruction]) + return CombinableInstructions( + signers=[new_open_orders_account], instructions=[instruction] + ) # # ๐Ÿฅญ build_serum_place_order_instructions function # # Creates a Serum order-placing instruction using V3 of the NewOrder instruction. # -def build_serum_place_order_instructions(context: Context, wallet: Wallet, market: PySerumMarket, source: PublicKey, open_orders_address: PublicKey, order_type: OrderType, side: Side, price: Decimal, quantity: Decimal, client_id: int, fee_discount_address: PublicKey) -> CombinableInstructions: - serum_order_type: PySerumOrderType = PySerumOrderType.POST_ONLY if order_type == OrderType.POST_ONLY else PySerumOrderType.IOC if order_type == OrderType.IOC else PySerumOrderType.LIMIT +def build_serum_place_order_instructions( + context: Context, + wallet: Wallet, + market: PySerumMarket, + source: PublicKey, + open_orders_address: PublicKey, + order_type: OrderType, + side: Side, + price: Decimal, + quantity: Decimal, + client_id: int, + fee_discount_address: PublicKey, +) -> CombinableInstructions: + serum_order_type: PySerumOrderType = ( + PySerumOrderType.POST_ONLY + if order_type == OrderType.POST_ONLY + else PySerumOrderType.IOC + if order_type == OrderType.IOC + else PySerumOrderType.LIMIT + ) serum_side: PySerumSide = PySerumSide.SELL if side == Side.SELL else PySerumSide.BUY instruction = market.make_place_order_instruction( @@ -151,7 +245,7 @@ def build_serum_place_order_instructions(context: Context, wallet: Wallet, marke float(quantity), client_id, open_orders_address, - fee_discount_address + fee_discount_address, ) return CombinableInstructions(signers=[], instructions=[instruction]) @@ -161,7 +255,13 @@ def build_serum_place_order_instructions(context: Context, wallet: Wallet, marke # # Creates an event-consuming 'crank' instruction. # -def build_serum_consume_events_instructions(context: Context, market_address: PublicKey, event_queue_address: PublicKey, open_orders_addresses: typing.Sequence[PublicKey], limit: int = 32) -> CombinableInstructions: +def build_serum_consume_events_instructions( + context: Context, + market_address: PublicKey, + event_queue_address: PublicKey, + open_orders_addresses: typing.Sequence[PublicKey], + limit: int = 32, +) -> CombinableInstructions: instruction = TransactionInstruction( keys=[ AccountMeta(pubkey=pubkey, is_signer=False, is_writable=True) @@ -169,15 +269,22 @@ def build_serum_consume_events_instructions(context: Context, market_address: Pu ], program_id=context.serum_program_address, data=PYSERUM_INSTRUCTIONS_LAYOUT.build( - dict(instruction_type=PySerumInstructionType.CONSUME_EVENTS, args=dict(limit=limit)) + dict( + instruction_type=PySerumInstructionType.CONSUME_EVENTS, + args=dict(limit=limit), + ) ), ) # The interface accepts (and currently requires) two accounts at the end, but # it doesn't actually use them. random_account = Keypair().public_key - instruction.keys.append(AccountMeta(random_account, is_signer=False, is_writable=True)) - instruction.keys.append(AccountMeta(random_account, is_signer=False, is_writable=True)) + instruction.keys.append( + AccountMeta(random_account, is_signer=False, is_writable=True) + ) + instruction.keys.append( + AccountMeta(random_account, is_signer=False, is_writable=True) + ) return CombinableInstructions(signers=[], instructions=[instruction]) @@ -185,9 +292,19 @@ def build_serum_consume_events_instructions(context: Context, market_address: Pu # # Creates a 'settle' instruction. # -def build_serum_settle_instructions(context: Context, wallet: Wallet, market: PySerumMarket, open_orders_address: PublicKey, base_token_account_address: PublicKey, quote_token_account_address: PublicKey) -> CombinableInstructions: +def build_serum_settle_instructions( + context: Context, + wallet: Wallet, + market: PySerumMarket, + open_orders_address: PublicKey, + base_token_account_address: PublicKey, + quote_token_account_address: PublicKey, +) -> CombinableInstructions: vault_signer = PublicKey.create_program_address( - [bytes(market.state.public_key()), market.state.vault_signer_nonce().to_bytes(8, byteorder="little")], + [ + bytes(market.state.public_key()), + market.state.vault_signer_nonce().to_bytes(8, byteorder="little"), + ], market.state.program_id(), ) instruction = pyserum_settle_funds( @@ -234,12 +351,23 @@ def build_serum_settle_instructions(context: Context, wallet: Wallet, market: Py # /// 16. `[]` dex_signer_ai - dex PySerumMarket signer account # /// 17. `[]` spl token program # -def build_spot_settle_instructions(context: Context, wallet: Wallet, account: Account, - market: PySerumMarket, group: Group, open_orders_address: PublicKey, - base_rootbank: RootBank, base_nodebank: NodeBank, - quote_rootbank: RootBank, quote_nodebank: NodeBank) -> CombinableInstructions: +def build_spot_settle_instructions( + context: Context, + wallet: Wallet, + account: Account, + market: PySerumMarket, + group: Group, + open_orders_address: PublicKey, + base_rootbank: RootBank, + base_nodebank: NodeBank, + quote_rootbank: RootBank, + quote_nodebank: NodeBank, +) -> CombinableInstructions: vault_signer = PublicKey.create_program_address( - [bytes(market.state.public_key()), market.state.vault_signer_nonce().to_bytes(8, byteorder="little")], + [ + bytes(market.state.public_key()), + market.state.vault_signer_nonce().to_bytes(8, byteorder="little"), + ], market.state.program_id(), ) @@ -249,23 +377,39 @@ def build_spot_settle_instructions(context: Context, wallet: Wallet, account: Ac AccountMeta(is_signer=False, is_writable=True, pubkey=group.cache), AccountMeta(is_signer=True, is_writable=False, pubkey=wallet.address), AccountMeta(is_signer=False, is_writable=True, pubkey=account.address), - AccountMeta(is_signer=False, is_writable=False, pubkey=context.serum_program_address), - AccountMeta(is_signer=False, is_writable=True, pubkey=market.state.public_key()), + AccountMeta( + is_signer=False, is_writable=False, pubkey=context.serum_program_address + ), + AccountMeta( + is_signer=False, is_writable=True, pubkey=market.state.public_key() + ), AccountMeta(is_signer=False, is_writable=True, pubkey=open_orders_address), AccountMeta(is_signer=False, is_writable=False, pubkey=group.signer_key), - AccountMeta(is_signer=False, is_writable=True, pubkey=market.state.base_vault()), - AccountMeta(is_signer=False, is_writable=True, pubkey=market.state.quote_vault()), - AccountMeta(is_signer=False, is_writable=False, pubkey=base_rootbank.address), - AccountMeta(is_signer=False, is_writable=True, pubkey=base_nodebank.address), - AccountMeta(is_signer=False, is_writable=False, pubkey=quote_rootbank.address), - AccountMeta(is_signer=False, is_writable=True, pubkey=quote_nodebank.address), + AccountMeta( + is_signer=False, is_writable=True, pubkey=market.state.base_vault() + ), + AccountMeta( + is_signer=False, is_writable=True, pubkey=market.state.quote_vault() + ), + AccountMeta( + is_signer=False, is_writable=False, pubkey=base_rootbank.address + ), + AccountMeta( + is_signer=False, is_writable=True, pubkey=base_nodebank.address + ), + AccountMeta( + is_signer=False, is_writable=False, pubkey=quote_rootbank.address + ), + AccountMeta( + is_signer=False, is_writable=True, pubkey=quote_nodebank.address + ), AccountMeta(is_signer=False, is_writable=True, pubkey=base_nodebank.vault), AccountMeta(is_signer=False, is_writable=True, pubkey=quote_nodebank.vault), AccountMeta(is_signer=False, is_writable=False, pubkey=vault_signer), - AccountMeta(is_signer=False, is_writable=False, pubkey=TOKEN_PROGRAM_ID) + AccountMeta(is_signer=False, is_writable=False, pubkey=TOKEN_PROGRAM_ID), ], program_id=context.mango_program_address, - data=layouts.SETTLE_FUNDS.build({}) + data=layouts.SETTLE_FUNDS.build({}), ) return CombinableInstructions(signers=[], instructions=[settle_instruction]) @@ -290,13 +434,51 @@ def build_spot_settle_instructions(context: Context, wallet: Wallet, account: Ac # immediately if the order is filled (either because it's IOC or because it matches an order on the # orderbook). # -def build_compound_serum_place_order_instructions(context: Context, wallet: Wallet, market: PySerumMarket, source: PublicKey, open_orders_address: PublicKey, all_open_orders_addresses: typing.Sequence[PublicKey], order_type: OrderType, side: Side, price: Decimal, quantity: Decimal, client_id: int, base_token_account_address: PublicKey, quote_token_account_address: PublicKey, fee_discount_address: PublicKey, consume_limit: int = 32) -> CombinableInstructions: +def build_compound_serum_place_order_instructions( + context: Context, + wallet: Wallet, + market: PySerumMarket, + source: PublicKey, + open_orders_address: PublicKey, + all_open_orders_addresses: typing.Sequence[PublicKey], + order_type: OrderType, + side: Side, + price: Decimal, + quantity: Decimal, + client_id: int, + base_token_account_address: PublicKey, + quote_token_account_address: PublicKey, + fee_discount_address: PublicKey, + consume_limit: int = 32, +) -> CombinableInstructions: place_order = build_serum_place_order_instructions( - context, wallet, market, source, open_orders_address, order_type, side, price, quantity, client_id, fee_discount_address) + context, + wallet, + market, + source, + open_orders_address, + order_type, + side, + price, + quantity, + client_id, + fee_discount_address, + ) consume_events = build_serum_consume_events_instructions( - context, market.state.public_key(), market.state.event_queue(), all_open_orders_addresses, consume_limit) + context, + market.state.public_key(), + market.state.event_queue(), + all_open_orders_addresses, + consume_limit, + ) settle = build_serum_settle_instructions( - context, wallet, market, open_orders_address, base_token_account_address, quote_token_account_address) + context, + wallet, + market, + open_orders_address, + base_token_account_address, + quote_token_account_address, + ) return place_order + consume_events + settle @@ -305,20 +487,23 @@ def build_compound_serum_place_order_instructions(context: Context, wallet: Wall # # Builds the instructions necessary for cancelling a perp order. # -def build_cancel_perp_order_instructions(context: Context, wallet: Wallet, account: Account, perp_market_details: PerpMarketDetails, order: Order, invalid_id_ok: bool) -> CombinableInstructions: +def build_cancel_perp_order_instructions( + context: Context, + wallet: Wallet, + account: Account, + perp_market_details: PerpMarketDetails, + order: Order, + invalid_id_ok: bool, +) -> CombinableInstructions: # Prefer cancelling by client ID so we don't have to keep track of the order side. if order.client_id is not None and order.client_id != 0: data: bytes = layouts.CANCEL_PERP_ORDER_BY_CLIENT_ID.build( - { - "client_order_id": order.client_id, - "invalid_id_ok": invalid_id_ok - }) + {"client_order_id": order.client_id, "invalid_id_ok": invalid_id_ok} + ) else: data = layouts.CANCEL_PERP_ORDER.build( - { - "order_id": order.id, - "invalid_id_ok": invalid_id_ok - }) + {"order_id": order.id, "invalid_id_ok": invalid_id_ok} + ) # Accounts expected by this instruction (both CANCEL_PERP_ORDER and CANCEL_PERP_ORDER_BY_CLIENT_ID are the same): # { isSigner: false, isWritable: false, pubkey: mangoGroupPk }, @@ -331,21 +516,44 @@ def build_cancel_perp_order_instructions(context: Context, wallet: Wallet, accou instructions = [ TransactionInstruction( keys=[ - AccountMeta(is_signer=False, is_writable=False, pubkey=account.group_address), + AccountMeta( + is_signer=False, is_writable=False, pubkey=account.group_address + ), AccountMeta(is_signer=False, is_writable=True, pubkey=account.address), AccountMeta(is_signer=True, is_writable=False, pubkey=wallet.address), - AccountMeta(is_signer=False, is_writable=True, pubkey=perp_market_details.address), - AccountMeta(is_signer=False, is_writable=True, pubkey=perp_market_details.bids), - AccountMeta(is_signer=False, is_writable=True, pubkey=perp_market_details.asks) + AccountMeta( + is_signer=False, + is_writable=True, + pubkey=perp_market_details.address, + ), + AccountMeta( + is_signer=False, is_writable=True, pubkey=perp_market_details.bids + ), + AccountMeta( + is_signer=False, is_writable=True, pubkey=perp_market_details.asks + ), ], program_id=context.mango_program_address, - data=data + data=data, ) ] return CombinableInstructions(signers=[], instructions=instructions) -def build_place_perp_order_instructions(context: Context, wallet: Wallet, group: Group, account: Account, perp_market_details: PerpMarketDetails, price: Decimal, quantity: Decimal, client_order_id: int, side: Side, order_type: OrderType, reduce_only: bool = False, reflink: typing.Optional[PublicKey] = None) -> CombinableInstructions: +def build_place_perp_order_instructions( + context: Context, + wallet: Wallet, + group: Group, + account: Account, + perp_market_details: PerpMarketDetails, + price: Decimal, + quantity: Decimal, + client_order_id: int, + side: Side, + order_type: OrderType, + reduce_only: bool = False, + reflink: typing.Optional[PublicKey] = None, +) -> CombinableInstructions: # { buy: 0, sell: 1 } raw_side: int = 1 if side == Side.SELL else 0 raw_order_type: int = order_type.to_perp() @@ -356,8 +564,9 @@ def build_place_perp_order_instructions(context: Context, wallet: Wallet, group: base_factor = Decimal(10) ** base_decimals quote_factor = Decimal(10) ** quote_decimals - native_price = ((price * quote_factor) * perp_market_details.base_lot_size) / \ - (perp_market_details.quote_lot_size * base_factor) + native_price = ((price * quote_factor) * perp_market_details.base_lot_size) / ( + perp_market_details.quote_lot_size * base_factor + ) native_quantity = (quantity * base_factor) / perp_market_details.base_lot_size # /// Accounts expected by this instruction (6): @@ -374,12 +583,24 @@ def build_place_perp_order_instructions(context: Context, wallet: Wallet, group: AccountMeta(is_signer=False, is_writable=True, pubkey=account.address), AccountMeta(is_signer=True, is_writable=False, pubkey=wallet.address), AccountMeta(is_signer=False, is_writable=False, pubkey=group.cache), - AccountMeta(is_signer=False, is_writable=True, pubkey=perp_market_details.address), + AccountMeta( + is_signer=False, is_writable=True, pubkey=perp_market_details.address + ), AccountMeta(is_signer=False, is_writable=True, pubkey=perp_market_details.bids), AccountMeta(is_signer=False, is_writable=True, pubkey=perp_market_details.asks), - AccountMeta(is_signer=False, is_writable=True, pubkey=perp_market_details.event_queue), - *list([AccountMeta(is_signer=False, is_writable=False, - pubkey=oo_address or SYSTEM_PROGRAM_ADDRESS) for oo_address in account.spot_open_orders_by_index[:-1]]) + AccountMeta( + is_signer=False, is_writable=True, pubkey=perp_market_details.event_queue + ), + *list( + [ + AccountMeta( + is_signer=False, + is_writable=False, + pubkey=oo_address or SYSTEM_PROGRAM_ADDRESS, + ) + for oo_address in account.spot_open_orders_by_index[:-1] + ] + ), ] if reflink is not None: keys += [AccountMeta(is_signer=False, is_writable=True, pubkey=reflink)] @@ -395,8 +616,9 @@ def build_place_perp_order_instructions(context: Context, wallet: Wallet, group: "client_order_id": client_order_id, "side": raw_side, "order_type": raw_order_type, - "reduce_only": reduce_only - }) + "reduce_only": reduce_only, + } + ), ) ] return CombinableInstructions(signers=[], instructions=instructions) @@ -406,7 +628,13 @@ def build_place_perp_order_instructions(context: Context, wallet: Wallet, group: # # Builds the instructions necessary for cancelling all perp orders. # -def build_cancel_all_perp_orders_instructions(context: Context, wallet: Wallet, account: Account, perp_market_details: PerpMarketDetails, limit: Decimal = Decimal(32)) -> CombinableInstructions: +def build_cancel_all_perp_orders_instructions( + context: Context, + wallet: Wallet, + account: Account, + perp_market_details: PerpMarketDetails, + limit: Decimal = Decimal(32), +) -> CombinableInstructions: # Accounts expected by this instruction (seems to be the same as CANCEL_PERP_ORDER and CANCEL_PERP_ORDER_BY_CLIENT_ID): # { isSigner: false, isWritable: false, pubkey: mangoGroupPk }, # { isSigner: false, isWritable: true, pubkey: mangoAccountPk }, @@ -417,24 +645,37 @@ def build_cancel_all_perp_orders_instructions(context: Context, wallet: Wallet, instructions = [ TransactionInstruction( keys=[ - AccountMeta(is_signer=False, is_writable=False, pubkey=account.group_address), + AccountMeta( + is_signer=False, is_writable=False, pubkey=account.group_address + ), AccountMeta(is_signer=False, is_writable=True, pubkey=account.address), AccountMeta(is_signer=True, is_writable=False, pubkey=wallet.address), - AccountMeta(is_signer=False, is_writable=True, pubkey=perp_market_details.address), - AccountMeta(is_signer=False, is_writable=True, pubkey=perp_market_details.bids), - AccountMeta(is_signer=False, is_writable=True, pubkey=perp_market_details.asks) + AccountMeta( + is_signer=False, + is_writable=True, + pubkey=perp_market_details.address, + ), + AccountMeta( + is_signer=False, is_writable=True, pubkey=perp_market_details.bids + ), + AccountMeta( + is_signer=False, is_writable=True, pubkey=perp_market_details.asks + ), ], program_id=context.mango_program_address, - data=layouts.CANCEL_ALL_PERP_ORDERS.build( - { - "limit": limit - }) + data=layouts.CANCEL_ALL_PERP_ORDERS.build({"limit": limit}), ) ] return CombinableInstructions(signers=[], instructions=instructions) -def build_mango_consume_events_instructions(context: Context, group: Group, perp_market_details: PerpMarketDetails, account_addresses: typing.Sequence[PublicKey], limit: Decimal = Decimal(32)) -> CombinableInstructions: +def build_mango_consume_events_instructions( + context: Context, + group: Group, + perp_market_details: PerpMarketDetails, + account_addresses: typing.Sequence[PublicKey], + limit: Decimal = Decimal(32), +) -> CombinableInstructions: # Accounts expected by this instruction: # { isSigner: false, isWritable: false, pubkey: mangoGroupPk }, # { isSigner: false, isWritable: false, pubkey: mangoCachePk }, @@ -451,30 +692,49 @@ def build_mango_consume_events_instructions(context: Context, group: Group, perp keys=[ AccountMeta(is_signer=False, is_writable=False, pubkey=group.address), AccountMeta(is_signer=False, is_writable=False, pubkey=group.cache), - AccountMeta(is_signer=False, is_writable=True, pubkey=perp_market_details.address), - AccountMeta(is_signer=False, is_writable=True, pubkey=perp_market_details.event_queue), - *list([AccountMeta(is_signer=False, is_writable=True, - pubkey=account_address) for account_address in account_addresses]) + AccountMeta( + is_signer=False, + is_writable=True, + pubkey=perp_market_details.address, + ), + AccountMeta( + is_signer=False, + is_writable=True, + pubkey=perp_market_details.event_queue, + ), + *list( + [ + AccountMeta( + is_signer=False, is_writable=True, pubkey=account_address + ) + for account_address in account_addresses + ] + ), ], program_id=context.mango_program_address, data=layouts.CONSUME_EVENTS.build( { "limit": limit, - }) + } + ), ) ] return CombinableInstructions(signers=[], instructions=instructions) # The old INIT_MANGO_ACCOUNT instruction is now superseded by CREATE_MANGO_ACCOUNT -def build_create_account_instructions(context: Context, wallet: Wallet, group: Group, account_num: Decimal = Decimal(1)) -> CombinableInstructions: - mango_account_address_and_nonce: typing.Tuple[PublicKey, int] = PublicKey.find_program_address( +def build_create_account_instructions( + context: Context, wallet: Wallet, group: Group, account_num: Decimal = Decimal(1) +) -> CombinableInstructions: + mango_account_address_and_nonce: typing.Tuple[ + PublicKey, int + ] = PublicKey.find_program_address( [ bytes(group.address), bytes(wallet.address), - int(account_num).to_bytes(8, "little") + int(account_num).to_bytes(8, "little"), ], - context.mango_program_address + context.mango_program_address, ) mango_account_address: PublicKey = mango_account_address_and_nonce[0] @@ -485,12 +745,16 @@ def build_create_account_instructions(context: Context, wallet: Wallet, group: G create = TransactionInstruction( keys=[ AccountMeta(is_signer=False, is_writable=True, pubkey=group.address), - AccountMeta(is_signer=False, is_writable=True, pubkey=mango_account_address), + AccountMeta( + is_signer=False, is_writable=True, pubkey=mango_account_address + ), AccountMeta(is_signer=True, is_writable=False, pubkey=wallet.address), - AccountMeta(is_signer=False, is_writable=False, pubkey=SYSTEM_PROGRAM_ADDRESS) + AccountMeta( + is_signer=False, is_writable=False, pubkey=SYSTEM_PROGRAM_ADDRESS + ), ], program_id=context.mango_program_address, - data=layouts.CREATE_MANGO_ACCOUNT.build({"account_num": account_num}) + data=layouts.CREATE_MANGO_ACCOUNT.build({"account_num": account_num}), ) return CombinableInstructions(signers=[], instructions=[create]) @@ -511,7 +775,15 @@ def build_create_account_instructions(context: Context, wallet: Wallet, group: G # Deposit { # quantity: u64, # }, -def build_deposit_instructions(context: Context, wallet: Wallet, group: Group, account: Account, root_bank: RootBank, node_bank: NodeBank, token_account: TokenAccount) -> CombinableInstructions: +def build_deposit_instructions( + context: Context, + wallet: Wallet, + group: Group, + account: Account, + root_bank: RootBank, + node_bank: NodeBank, + token_account: TokenAccount, +) -> CombinableInstructions: value = token_account.value.shift_to_native().value deposit = TransactionInstruction( keys=[ @@ -523,12 +795,12 @@ def build_deposit_instructions(context: Context, wallet: Wallet, group: Group, a AccountMeta(is_signer=False, is_writable=True, pubkey=node_bank.address), AccountMeta(is_signer=False, is_writable=True, pubkey=node_bank.vault), AccountMeta(is_signer=False, is_writable=False, pubkey=TOKEN_PROGRAM_ID), - AccountMeta(is_signer=False, is_writable=True, pubkey=token_account.address) + AccountMeta( + is_signer=False, is_writable=True, pubkey=token_account.address + ), ], program_id=context.mango_program_address, - data=layouts.DEPOSIT.build({ - "quantity": value - }) + data=layouts.DEPOSIT.build({"quantity": value}), ) return CombinableInstructions(signers=[], instructions=[deposit]) @@ -554,7 +826,16 @@ def build_deposit_instructions(context: Context, wallet: Wallet, group: Group, a # quantity: u64, # allow_borrow: bool, # }, -def build_withdraw_instructions(context: Context, wallet: Wallet, group: Group, account: Account, root_bank: RootBank, node_bank: NodeBank, token_account: TokenAccount, allow_borrow: bool) -> CombinableInstructions: +def build_withdraw_instructions( + context: Context, + wallet: Wallet, + group: Group, + account: Account, + root_bank: RootBank, + node_bank: NodeBank, + token_account: TokenAccount, + allow_borrow: bool, +) -> CombinableInstructions: value = token_account.value.shift_to_native().value withdraw = TransactionInstruction( keys=[ @@ -565,27 +846,42 @@ def build_withdraw_instructions(context: Context, wallet: Wallet, group: Group, AccountMeta(is_signer=False, is_writable=False, pubkey=root_bank.address), AccountMeta(is_signer=False, is_writable=True, pubkey=node_bank.address), AccountMeta(is_signer=False, is_writable=True, pubkey=node_bank.vault), - AccountMeta(is_signer=False, is_writable=True, pubkey=token_account.address), + AccountMeta( + is_signer=False, is_writable=True, pubkey=token_account.address + ), AccountMeta(is_signer=False, is_writable=False, pubkey=group.signer_key), AccountMeta(is_signer=False, is_writable=False, pubkey=TOKEN_PROGRAM_ID), - *list([AccountMeta(is_signer=False, is_writable=False, - pubkey=oo_address or SYSTEM_PROGRAM_ADDRESS) for oo_address in account.spot_open_orders_by_index[:-1]]) + *list( + [ + AccountMeta( + is_signer=False, + is_writable=False, + pubkey=oo_address or SYSTEM_PROGRAM_ADDRESS, + ) + for oo_address in account.spot_open_orders_by_index[:-1] + ] + ), ], program_id=context.mango_program_address, - data=layouts.WITHDRAW.build({ - "quantity": value, - "allow_borrow": allow_borrow - }) + data=layouts.WITHDRAW.build({"quantity": value, "allow_borrow": allow_borrow}), ) return CombinableInstructions(signers=[], instructions=[withdraw]) -def build_spot_openorders_instructions(context: Context, wallet: Wallet, group: Group, account: Account, spot_market: SpotMarket) -> CombinableInstructions: +def build_spot_openorders_instructions( + context: Context, + wallet: Wallet, + group: Group, + account: Account, + spot_market: SpotMarket, +) -> CombinableInstructions: instructions: CombinableInstructions = CombinableInstructions.empty() # Spot OpenOrders accounts use a PDA as of v3.3 - open_orders_address: PublicKey = spot_market.derive_open_orders_address(context, account) + open_orders_address: PublicKey = spot_market.derive_open_orders_address( + context, account + ) # /// Accounts expected by this instruction (8): # /// @@ -602,16 +898,22 @@ def build_spot_openorders_instructions(context: Context, wallet: Wallet, group: AccountMeta(is_signer=False, is_writable=False, pubkey=group.address), AccountMeta(is_signer=False, is_writable=True, pubkey=account.address), AccountMeta(is_signer=True, is_writable=False, pubkey=wallet.address), - AccountMeta(is_signer=False, is_writable=False, pubkey=context.serum_program_address), + AccountMeta( + is_signer=False, is_writable=False, pubkey=context.serum_program_address + ), AccountMeta(is_signer=False, is_writable=True, pubkey=open_orders_address), AccountMeta(is_signer=False, is_writable=False, pubkey=spot_market.address), AccountMeta(is_signer=False, is_writable=False, pubkey=group.signer_key), - AccountMeta(is_signer=False, is_writable=False, pubkey=SYSTEM_PROGRAM_ADDRESS) + AccountMeta( + is_signer=False, is_writable=False, pubkey=SYSTEM_PROGRAM_ADDRESS + ), ], program_id=context.mango_program_address, - data=layouts.CREATE_SPOT_OPEN_ORDERS.build({}) + data=layouts.CREATE_SPOT_OPEN_ORDERS.build({}), + ) + instructions += CombinableInstructions( + signers=[], instructions=[create_open_orders_instruction] ) - instructions += CombinableInstructions(signers=[], instructions=[create_open_orders_instruction]) return instructions @@ -648,10 +950,19 @@ def build_spot_openorders_instructions(context: Context, wallet: Wallet, group: # isWritable, # pubkey, # })), -def build_spot_place_order_instructions(context: Context, wallet: Wallet, group: Group, account: Account, - spot_market: SpotMarket, order_type: OrderType, side: Side, - price: Decimal, quantity: Decimal, client_id: int, - fee_discount_address: PublicKey) -> CombinableInstructions: +def build_spot_place_order_instructions( + context: Context, + wallet: Wallet, + group: Group, + account: Account, + spot_market: SpotMarket, + order_type: OrderType, + side: Side, + price: Decimal, + quantity: Decimal, + client_id: int, + fee_discount_address: PublicKey, +) -> CombinableInstructions: instructions: CombinableInstructions = CombinableInstructions.empty() slot = group.slot_by_spot_market_address(spot_market.address) @@ -660,7 +971,9 @@ def build_spot_place_order_instructions(context: Context, wallet: Wallet, group: open_orders_address = account.spot_open_orders_by_index[market_index] if open_orders_address is None: - create_open_orders = build_spot_openorders_instructions(context, wallet, group, account, spot_market) + create_open_orders = build_spot_openorders_instructions( + context, wallet, group, account, spot_market + ) instructions += create_open_orders open_orders_address = spot_market.derive_open_orders_address(context, account) @@ -673,14 +986,22 @@ def build_spot_place_order_instructions(context: Context, wallet: Wallet, group: serum_side: pyserum.enums.Side = side.to_serum() intrinsic_price = pyserum_market.state.price_number_to_lots(float(price)) max_base_quantity = pyserum_market.state.base_size_number_to_lots(float(quantity)) - max_quote_quantity = pyserum_market.state.base_size_number_to_lots( - float(quantity)) * pyserum_market.state.quote_lot_size() * pyserum_market.state.price_number_to_lots(float(price)) + max_quote_quantity = ( + pyserum_market.state.base_size_number_to_lots(float(quantity)) + * pyserum_market.state.quote_lot_size() + * pyserum_market.state.price_number_to_lots(float(price)) + ) base_token_banks = [ - token_bank for token_bank in group.base_tokens_by_index if token_bank is not None and token_bank.token.mint == pyserum_market.state.base_mint()] + token_bank + for token_bank in group.base_tokens_by_index + if token_bank is not None + and token_bank.token.mint == pyserum_market.state.base_mint() + ] if len(base_token_banks) != 1: raise Exception( - f"Could not find base token info for group {group.address} - length was {len(base_token_banks)} when it should be 1.") + f"Could not find base token info for group {group.address} - length was {len(base_token_banks)} when it should be 1." + ) base_token_bank = base_token_banks[0] quote_token_bank = group.shared_quote @@ -692,7 +1013,7 @@ def build_spot_place_order_instructions(context: Context, wallet: Wallet, group: vault_signer = PublicKey.create_program_address( [ bytes(spot_market.address), - pyserum_market.state.vault_signer_nonce().to_bytes(8, byteorder="little") + pyserum_market.state.vault_signer_nonce().to_bytes(8, byteorder="little"), ], pyserum_market.state.program_id(), ) @@ -703,26 +1024,68 @@ def build_spot_place_order_instructions(context: Context, wallet: Wallet, group: AccountMeta(is_signer=False, is_writable=True, pubkey=account.address), AccountMeta(is_signer=True, is_writable=False, pubkey=wallet.address), AccountMeta(is_signer=False, is_writable=False, pubkey=group.cache), - AccountMeta(is_signer=False, is_writable=False, pubkey=context.serum_program_address), + AccountMeta( + is_signer=False, is_writable=False, pubkey=context.serum_program_address + ), AccountMeta(is_signer=False, is_writable=True, pubkey=spot_market.address), - AccountMeta(is_signer=False, is_writable=True, pubkey=spot_market.bids_address), - AccountMeta(is_signer=False, is_writable=True, pubkey=spot_market.asks_address), - AccountMeta(is_signer=False, is_writable=True, pubkey=pyserum_market.state.request_queue()), - AccountMeta(is_signer=False, is_writable=True, pubkey=spot_market.event_queue_address), - AccountMeta(is_signer=False, is_writable=True, pubkey=pyserum_market.state.base_vault()), - AccountMeta(is_signer=False, is_writable=True, pubkey=pyserum_market.state.quote_vault()), - AccountMeta(is_signer=False, is_writable=False, pubkey=base_root_bank.address), - AccountMeta(is_signer=False, is_writable=True, pubkey=base_node_bank.address), + AccountMeta( + is_signer=False, is_writable=True, pubkey=spot_market.bids_address + ), + AccountMeta( + is_signer=False, is_writable=True, pubkey=spot_market.asks_address + ), + AccountMeta( + is_signer=False, + is_writable=True, + pubkey=pyserum_market.state.request_queue(), + ), + AccountMeta( + is_signer=False, + is_writable=True, + pubkey=spot_market.event_queue_address, + ), + AccountMeta( + is_signer=False, + is_writable=True, + pubkey=pyserum_market.state.base_vault(), + ), + AccountMeta( + is_signer=False, + is_writable=True, + pubkey=pyserum_market.state.quote_vault(), + ), + AccountMeta( + is_signer=False, is_writable=False, pubkey=base_root_bank.address + ), + AccountMeta( + is_signer=False, is_writable=True, pubkey=base_node_bank.address + ), AccountMeta(is_signer=False, is_writable=True, pubkey=base_node_bank.vault), - AccountMeta(is_signer=False, is_writable=False, pubkey=quote_root_bank.address), - AccountMeta(is_signer=False, is_writable=True, pubkey=quote_node_bank.address), - AccountMeta(is_signer=False, is_writable=True, pubkey=quote_node_bank.vault), + AccountMeta( + is_signer=False, is_writable=False, pubkey=quote_root_bank.address + ), + AccountMeta( + is_signer=False, is_writable=True, pubkey=quote_node_bank.address + ), + AccountMeta( + is_signer=False, is_writable=True, pubkey=quote_node_bank.vault + ), AccountMeta(is_signer=False, is_writable=False, pubkey=TOKEN_PROGRAM_ID), AccountMeta(is_signer=False, is_writable=False, pubkey=group.signer_key), AccountMeta(is_signer=False, is_writable=False, pubkey=vault_signer), - AccountMeta(is_signer=False, is_writable=False, pubkey=fee_discount_address), - *list([AccountMeta(is_signer=False, is_writable=(oo_address == open_orders_address), - pubkey=oo_address) for oo_address in account.spot_open_orders]) + AccountMeta( + is_signer=False, is_writable=False, pubkey=fee_discount_address + ), + *list( + [ + AccountMeta( + is_signer=False, + is_writable=(oo_address == open_orders_address), + pubkey=oo_address, + ) + for oo_address in account.spot_open_orders + ] + ), ], program_id=context.mango_program_address, data=layouts.PLACE_SPOT_ORDER_2.build( @@ -736,17 +1099,27 @@ def build_spot_place_order_instructions(context: Context, wallet: Wallet, group: client_id=client_id, limit=65535, ) - ) + ), ) - return instructions + CombinableInstructions(signers=[], instructions=[place_spot_instruction]) + return instructions + CombinableInstructions( + signers=[], instructions=[place_spot_instruction] + ) # # ๐Ÿฅญ build_cancel_spot_order_instruction function # # Builds the instructions necessary for cancelling a spot order. # -def build_cancel_spot_order_instructions(context: Context, wallet: Wallet, group: Group, account: Account, market: PySerumMarket, order: Order, open_orders_address: PublicKey) -> CombinableInstructions: +def build_cancel_spot_order_instructions( + context: Context, + wallet: Wallet, + group: Group, + account: Account, + market: PySerumMarket, + order: Order, + open_orders_address: PublicKey, +) -> CombinableInstructions: # { buy: 0, sell: 1 } raw_side: int = 1 if order.side == Side.SELL else 0 @@ -768,20 +1141,34 @@ def build_cancel_spot_order_instructions(context: Context, wallet: Wallet, group AccountMeta(is_signer=False, is_writable=False, pubkey=group.address), AccountMeta(is_signer=True, is_writable=False, pubkey=wallet.address), AccountMeta(is_signer=False, is_writable=False, pubkey=account.address), - AccountMeta(is_signer=False, is_writable=False, pubkey=context.serum_program_address), - AccountMeta(is_signer=False, is_writable=True, pubkey=market.state.public_key()), - AccountMeta(is_signer=False, is_writable=True, pubkey=market.state.bids()), - AccountMeta(is_signer=False, is_writable=True, pubkey=market.state.asks()), - AccountMeta(is_signer=False, is_writable=True, pubkey=open_orders_address), - AccountMeta(is_signer=False, is_writable=False, pubkey=group.signer_key), - AccountMeta(is_signer=False, is_writable=True, pubkey=market.state.event_queue()) + AccountMeta( + is_signer=False, + is_writable=False, + pubkey=context.serum_program_address, + ), + AccountMeta( + is_signer=False, is_writable=True, pubkey=market.state.public_key() + ), + AccountMeta( + is_signer=False, is_writable=True, pubkey=market.state.bids() + ), + AccountMeta( + is_signer=False, is_writable=True, pubkey=market.state.asks() + ), + AccountMeta( + is_signer=False, is_writable=True, pubkey=open_orders_address + ), + AccountMeta( + is_signer=False, is_writable=False, pubkey=group.signer_key + ), + AccountMeta( + is_signer=False, is_writable=True, pubkey=market.state.event_queue() + ), ], program_id=context.mango_program_address, data=layouts.CANCEL_SPOT_ORDER.build( - { - "order_id": order.id, - "side": raw_side - }) + {"order_id": order.id, "side": raw_side} + ), ) ] return CombinableInstructions(signers=[], instructions=instructions) @@ -791,9 +1178,19 @@ def build_cancel_spot_order_instructions(context: Context, wallet: Wallet, group # # Creates a 'settle' instruction for Mango accounts. # -def build_mango_settle_instructions(context: Context, wallet: Wallet, market: PySerumMarket, open_orders_address: PublicKey, base_token_account_address: PublicKey, quote_token_account_address: PublicKey) -> CombinableInstructions: +def build_mango_settle_instructions( + context: Context, + wallet: Wallet, + market: PySerumMarket, + open_orders_address: PublicKey, + base_token_account_address: PublicKey, + quote_token_account_address: PublicKey, +) -> CombinableInstructions: vault_signer = PublicKey.create_program_address( - [bytes(market.state.public_key()), market.state.vault_signer_nonce().to_bytes(8, byteorder="little")], + [ + bytes(market.state.public_key()), + market.state.vault_signer_nonce().to_bytes(8, byteorder="little"), + ], market.state.program_id(), ) instruction = pyserum_settle_funds( @@ -817,7 +1214,14 @@ def build_mango_settle_instructions(context: Context, wallet: Wallet, market: Py # # Creates a 'RedeemMngo' instruction for Mango accounts. # -def build_redeem_accrued_mango_instructions(context: Context, wallet: Wallet, perp_market: PerpMarket, group: Group, account: Account, mngo: TokenBank) -> CombinableInstructions: +def build_redeem_accrued_mango_instructions( + context: Context, + wallet: Wallet, + perp_market: PerpMarket, + group: Group, + account: Account, + mngo: TokenBank, +) -> CombinableInstructions: node_bank: NodeBank = mngo.pick_node_bank(context) # /// Redeem the mngo_accrued in a PerpAccount for MNGO in MangoAccount deposits # /// @@ -840,28 +1244,39 @@ def build_redeem_accrued_mango_instructions(context: Context, wallet: Wallet, pe AccountMeta(is_signer=False, is_writable=True, pubkey=account.address), AccountMeta(is_signer=True, is_writable=False, pubkey=wallet.address), AccountMeta(is_signer=False, is_writable=False, pubkey=perp_market.address), - AccountMeta(is_signer=False, is_writable=True, pubkey=perp_market.underlying_perp_market.mngo_vault), - AccountMeta(is_signer=False, is_writable=False, pubkey=mngo.root_bank_address), + AccountMeta( + is_signer=False, + is_writable=True, + pubkey=perp_market.underlying_perp_market.mngo_vault, + ), + AccountMeta( + is_signer=False, is_writable=False, pubkey=mngo.root_bank_address + ), AccountMeta(is_signer=False, is_writable=True, pubkey=node_bank.address), AccountMeta(is_signer=False, is_writable=True, pubkey=node_bank.vault), AccountMeta(is_signer=False, is_writable=False, pubkey=group.signer_key), - AccountMeta(is_signer=False, is_writable=False, pubkey=TOKEN_PROGRAM_ID) + AccountMeta(is_signer=False, is_writable=False, pubkey=TOKEN_PROGRAM_ID), ], program_id=context.mango_program_address, - data=layouts.REDEEM_MNGO.build({}) + data=layouts.REDEEM_MNGO.build({}), + ) + return CombinableInstructions( + signers=[], instructions=[redeem_accrued_mango_instruction] ) - return CombinableInstructions(signers=[], instructions=[redeem_accrued_mango_instruction]) # # ๐Ÿฅญ build_faucet_airdrop_instructions function # # Creates an airdrop instruction for compatible faucets (those based on https://github.com/paul-schaaf/spl-token-faucet) # -def build_faucet_airdrop_instructions(token_mint: PublicKey, destination: PublicKey, faucet: PublicKey, quantity: Decimal) -> CombinableInstructions: - faucet_program_address: PublicKey = PublicKey("4bXpkKSV8swHSnwqtzuboGPaPDeEgAn4Vt8GfarV5rZt") +def build_faucet_airdrop_instructions( + token_mint: PublicKey, destination: PublicKey, faucet: PublicKey, quantity: Decimal +) -> CombinableInstructions: + faucet_program_address: PublicKey = PublicKey( + "4bXpkKSV8swHSnwqtzuboGPaPDeEgAn4Vt8GfarV5rZt" + ) authority_and_nonce: typing.Tuple[PublicKey, int] = PublicKey.find_program_address( - [b"faucet"], - faucet_program_address + [b"faucet"], faucet_program_address ) authority: PublicKey = authority_and_nonce[0] @@ -885,12 +1300,10 @@ def build_faucet_airdrop_instructions(token_mint: PublicKey, destination: Public AccountMeta(is_signer=False, is_writable=True, pubkey=token_mint), AccountMeta(is_signer=False, is_writable=True, pubkey=destination), AccountMeta(is_signer=False, is_writable=False, pubkey=TOKEN_PROGRAM_ID), - AccountMeta(is_signer=False, is_writable=False, pubkey=faucet) + AccountMeta(is_signer=False, is_writable=False, pubkey=faucet), ], program_id=faucet_program_address, - data=layouts.FAUCET_AIRDROP.build({ - "quantity": quantity - }) + data=layouts.FAUCET_AIRDROP.build({"quantity": quantity}), ) return CombinableInstructions(signers=[], instructions=[faucet_airdrop_instruction]) @@ -902,7 +1315,13 @@ def build_faucet_airdrop_instructions(token_mint: PublicKey, destination: Public # # Set to SYSTEM_PROGRAM_ADDRESS to revoke delegate. # -def build_set_account_delegate_instructions(context: Context, wallet: Wallet, group: Group, account: Account, delegate: PublicKey) -> CombinableInstructions: +def build_set_account_delegate_instructions( + context: Context, + wallet: Wallet, + group: Group, + account: Account, + delegate: PublicKey, +) -> CombinableInstructions: # /// https://github.com/blockworks-foundation/mango-v3/pull/97/ # /// Set delegate authority to mango account which can do everything regular account can do # /// except Withdraw and CloseMangoAccount. Set to Pubkey::default() to revoke delegate @@ -917,16 +1336,20 @@ def build_set_account_delegate_instructions(context: Context, wallet: Wallet, gr AccountMeta(is_signer=False, is_writable=False, pubkey=group.address), AccountMeta(is_signer=False, is_writable=True, pubkey=account.address), AccountMeta(is_signer=True, is_writable=False, pubkey=wallet.address), - AccountMeta(is_signer=False, is_writable=False, pubkey=delegate) + AccountMeta(is_signer=False, is_writable=False, pubkey=delegate), ], program_id=context.mango_program_address, - data=layouts.SET_DELEGATE.build({}) + data=layouts.SET_DELEGATE.build({}), ) return CombinableInstructions(signers=[], instructions=[set_delegate_instruction]) -def build_unset_account_delegate_instructions(context: Context, wallet: Wallet, group: Group, account: Account) -> CombinableInstructions: - return build_set_account_delegate_instructions(context, wallet, group, account, SYSTEM_PROGRAM_ADDRESS) +def build_unset_account_delegate_instructions( + context: Context, wallet: Wallet, group: Group, account: Account +) -> CombinableInstructions: + return build_set_account_delegate_instructions( + context, wallet, group, account, SYSTEM_PROGRAM_ADDRESS + ) # # ๐Ÿฅญ build_set_referrer_memory_instructions function @@ -934,7 +1357,14 @@ def build_unset_account_delegate_instructions(context: Context, wallet: Wallet, # Creates an instruction to store the referrer's MangoAccount pubkey on the Referrer account # and create the Referrer account as a PDA of user's MangoAccount if it doesn't exist # -def build_set_referrer_memory_instructions(context: Context, wallet: Wallet, group: Group, account: Account, referrer_memory_address: PublicKey, referrer_account_address: PublicKey) -> CombinableInstructions: +def build_set_referrer_memory_instructions( + context: Context, + wallet: Wallet, + group: Group, + account: Account, + referrer_memory_address: PublicKey, + referrer_account_address: PublicKey, +) -> CombinableInstructions: # /// Store the referrer's MangoAccount pubkey on the Referrer account # /// It will create the Referrer account as a PDA of user's MangoAccount if it doesn't exist # /// This is primarily useful for the UI; the referrer address stored here is not necessarily @@ -954,22 +1384,37 @@ def build_set_referrer_memory_instructions(context: Context, wallet: Wallet, gro AccountMeta(is_signer=False, is_writable=False, pubkey=group.address), AccountMeta(is_signer=False, is_writable=False, pubkey=account.address), AccountMeta(is_signer=True, is_writable=False, pubkey=wallet.address), - AccountMeta(is_signer=False, is_writable=True, pubkey=referrer_memory_address), - AccountMeta(is_signer=False, is_writable=True, pubkey=referrer_account_address), + AccountMeta( + is_signer=False, is_writable=True, pubkey=referrer_memory_address + ), + AccountMeta( + is_signer=False, is_writable=True, pubkey=referrer_account_address + ), AccountMeta(is_signer=True, is_writable=True, pubkey=wallet.address), - AccountMeta(is_signer=False, is_writable=False, pubkey=SYSTEM_PROGRAM_ADDRESS) + AccountMeta( + is_signer=False, is_writable=False, pubkey=SYSTEM_PROGRAM_ADDRESS + ), ], program_id=context.mango_program_address, - data=layouts.SET_REFERRER_MEMORY.build({}) + data=layouts.SET_REFERRER_MEMORY.build({}), + ) + return CombinableInstructions( + signers=[], instructions=[set_referrer_memory_instruction] ) - return CombinableInstructions(signers=[], instructions=[set_referrer_memory_instruction]) # # ๐Ÿฅญ build_register_referrer_id_instructions function # # Creates an instruction to register a 'referrer ID' for a Mango Account # -def build_register_referrer_id_instructions(context: Context, wallet: Wallet, group: Group, account: Account, referrer_record_address: PublicKey, referrer_id: str) -> CombinableInstructions: +def build_register_referrer_id_instructions( + context: Context, + wallet: Wallet, + group: Group, + account: Account, + referrer_record_address: PublicKey, + referrer_id: str, +) -> CombinableInstructions: # /// Associate the referrer's MangoAccount with a human readable `referrer_id` which can be used # /// in a ref link. This is primarily useful for the UI. # /// Create the `ReferrerIdRecord` PDA; if it already exists throw error @@ -984,13 +1429,17 @@ def build_register_referrer_id_instructions(context: Context, wallet: Wallet, gr keys=[ AccountMeta(is_signer=False, is_writable=False, pubkey=group.address), AccountMeta(is_signer=False, is_writable=False, pubkey=account.address), - AccountMeta(is_signer=False, is_writable=True, pubkey=referrer_record_address), + AccountMeta( + is_signer=False, is_writable=True, pubkey=referrer_record_address + ), AccountMeta(is_signer=True, is_writable=True, pubkey=wallet.address), - AccountMeta(is_signer=False, is_writable=False, pubkey=SYSTEM_PROGRAM_ADDRESS) + AccountMeta( + is_signer=False, is_writable=False, pubkey=SYSTEM_PROGRAM_ADDRESS + ), ], program_id=context.mango_program_address, - data=layouts.REGISTER_REFERRER_ID.build({ - "info": referrer_id - }) + data=layouts.REGISTER_REFERRER_ID.build({"info": referrer_id}), + ) + return CombinableInstructions( + signers=[], instructions=[register_referrer_id_instruction] ) - return CombinableInstructions(signers=[], instructions=[register_referrer_id_instruction]) diff --git a/mango/instrumentlookup.py b/mango/instrumentlookup.py index 9360044..616c451 100644 --- a/mango/instrumentlookup.py +++ b/mango/instrumentlookup.py @@ -39,11 +39,15 @@ class InstrumentLookup(metaclass=abc.ABCMeta): @abc.abstractmethod def find_by_symbol(self, symbol: str) -> typing.Optional[Instrument]: - raise NotImplementedError("InstrumentLookup.find_by_symbol() is not implemented on the base type.") + raise NotImplementedError( + "InstrumentLookup.find_by_symbol() is not implemented on the base type." + ) @abc.abstractmethod def find_by_mint(self, mint: PublicKey) -> typing.Optional[Instrument]: - raise NotImplementedError("InstrumentLookup.find_by_mint() is not implemented on the base type.") + raise NotImplementedError( + "InstrumentLookup.find_by_mint() is not implemented on the base type." + ) def find_by_symbol_or_raise(self, symbol: str) -> Instrument: token = self.find_by_symbol(symbol) @@ -109,7 +113,9 @@ class CompoundInstrumentLookup(InstrumentLookup): return None def __str__(self) -> str: - inner = "\n ".join([f"{item}".replace("\n", "\n ") for item in self.lookups]) + inner = "\n ".join( + [f"{item}".replace("\n", "\n ") for item in self.lookups] + ) return f"""ยซ CompoundInstrumentLookup {inner} ยป""" @@ -129,7 +135,9 @@ class CompoundInstrumentLookup(InstrumentLookup): # class NonSPLInstrumentLookup(InstrumentLookup): DefaultMainnetDataFilepath = os.path.join(DATA_PATH, "nonspl.instrumentlist.json") - DefaultDevnetDataFilepath = os.path.join(DATA_PATH, "nonspl.instrumentlist.devnet.json") + DefaultDevnetDataFilepath = os.path.join( + DATA_PATH, "nonspl.instrumentlist.devnet.json" + ) def __init__(self, filename: str, token_data: typing.Dict[str, typing.Any]) -> None: super().__init__() @@ -139,7 +147,9 @@ class NonSPLInstrumentLookup(InstrumentLookup): def find_by_symbol(self, symbol: str) -> typing.Optional[Instrument]: for token in self.token_data["tokens"]: if Instrument.symbols_match(token["symbol"], symbol): - return Instrument(token["symbol"], token["name"], Decimal(token["decimals"])) + return Instrument( + token["symbol"], token["name"], Decimal(token["decimals"]) + ) return None @@ -168,19 +178,35 @@ class IdsJsonTokenLookup(InstrumentLookup): def find_by_symbol(self, symbol: str) -> typing.Optional[Token]: for group in MangoConstants["groups"]: - if group["cluster"] == self.cluster_name and group["name"] == self.group_name: + if ( + group["cluster"] == self.cluster_name + and group["name"] == self.group_name + ): for token in group["tokens"]: if Instrument.symbols_match(token["symbol"], symbol): - return Token(token["symbol"], token["symbol"], Decimal(token["decimals"]), PublicKey(token["mintKey"])) + return Token( + token["symbol"], + token["symbol"], + Decimal(token["decimals"]), + PublicKey(token["mintKey"]), + ) return None def find_by_mint(self, mint: PublicKey) -> typing.Optional[Token]: mint_str = str(mint) for group in MangoConstants["groups"]: - if group["cluster"] == self.cluster_name and group["name"] == self.group_name: + if ( + group["cluster"] == self.cluster_name + and group["name"] == self.group_name + ): for token in group["tokens"]: if token["mintKey"] == mint_str: - return Token(token["symbol"], token["symbol"], Decimal(token["decimals"]), PublicKey(token["mintKey"])) + return Token( + token["symbol"], + token["symbol"], + Decimal(token["decimals"]), + PublicKey(token["mintKey"]), + ) return None def __str__(self) -> str: @@ -204,7 +230,9 @@ class SPLTokenLookup(InstrumentLookup): DefaultDataFilepath = os.path.join(DATA_PATH, "solana.tokenlist.json") DevnetDataFilepath = os.path.join(DATA_PATH, "solana.tokenlist.devnet.json") OverridesDataFilepath = os.path.join(DATA_PATH, "overrides.tokenlist.json") - DevnetOverridesDataFilepath = os.path.join(DATA_PATH, "overrides.tokenlist.devnet.json") + DevnetOverridesDataFilepath = os.path.join( + DATA_PATH, "overrides.tokenlist.devnet.json" + ) def __init__(self, filename: str, token_data: typing.Dict[str, typing.Any]) -> None: super().__init__() @@ -214,7 +242,12 @@ class SPLTokenLookup(InstrumentLookup): def find_by_symbol(self, symbol: str) -> typing.Optional[Token]: for token in self.token_data["tokens"]: if Instrument.symbols_match(token["symbol"], symbol): - return Token(token["symbol"], token["name"], Decimal(token["decimals"]), PublicKey(token["address"])) + return Token( + token["symbol"], + token["name"], + Decimal(token["decimals"]), + PublicKey(token["address"]), + ) return None @@ -222,7 +255,12 @@ class SPLTokenLookup(InstrumentLookup): mint_string: str = str(mint) for token in self.token_data["tokens"]: if token["address"] == mint_string: - return Token(token["symbol"], token["name"], Decimal(token["decimals"]), PublicKey(token["address"])) + return Token( + token["symbol"], + token["name"], + Decimal(token["decimals"]), + PublicKey(token["address"]), + ) return None diff --git a/mango/instrumentvalue.py b/mango/instrumentvalue.py index bbca3bd..f26da19 100644 --- a/mango/instrumentvalue.py +++ b/mango/instrumentvalue.py @@ -59,54 +59,83 @@ class InstrumentValue: return InstrumentValue(self.token, new_value) @staticmethod - def fetch_total_value_or_none(context: Context, account_public_key: PublicKey, token: Token) -> typing.Optional["InstrumentValue"]: + def fetch_total_value_or_none( + context: Context, account_public_key: PublicKey, token: Token + ) -> typing.Optional["InstrumentValue"]: opts = TokenAccountOpts(mint=token.mint) - token_accounts = context.client.get_token_accounts_by_owner(account_public_key, opts) + token_accounts = context.client.get_token_accounts_by_owner( + account_public_key, opts + ) if len(token_accounts) == 0: return None total_value = Decimal(0) for token_account in token_accounts: - token_balance: Decimal = context.client.get_token_account_balance(token_account["pubkey"]) + token_balance: Decimal = context.client.get_token_account_balance( + token_account["pubkey"] + ) total_value += token_balance return InstrumentValue(token, total_value) @staticmethod - def fetch_total_value(context: Context, account_public_key: PublicKey, token: Token) -> "InstrumentValue": - value = InstrumentValue.fetch_total_value_or_none(context, account_public_key, token) + def fetch_total_value( + context: Context, account_public_key: PublicKey, token: Token + ) -> "InstrumentValue": + value = InstrumentValue.fetch_total_value_or_none( + context, account_public_key, token + ) if value is None: return InstrumentValue(token, Decimal(0)) return value @staticmethod - def report(values: typing.Sequence["InstrumentValue"], reporter: typing.Callable[[str], None] = output) -> None: + def report( + values: typing.Sequence["InstrumentValue"], + reporter: typing.Callable[[str], None] = output, + ) -> None: for value in values: reporter(f"{value.value:>18,.8f} {value.token.name}") @staticmethod - def find_by_symbol(values: typing.Sequence[typing.Optional["InstrumentValue"]], symbol: str) -> "InstrumentValue": + def find_by_symbol( + values: typing.Sequence[typing.Optional["InstrumentValue"]], symbol: str + ) -> "InstrumentValue": found = [ - value for value in values if value is not None and value.token is not None and value.token.symbol_matches(symbol)] + value + for value in values + if value is not None + and value.token is not None + and value.token.symbol_matches(symbol) + ] if len(found) == 0: raise Exception(f"Token '{symbol}' not found in token values: {values}") if len(found) > 1: - raise Exception(f"Token '{symbol}' matched multiple tokens in values: {values}") + raise Exception( + f"Token '{symbol}' matched multiple tokens in values: {values}" + ) return found[0] @staticmethod - def find_by_token(values: typing.Sequence[typing.Optional["InstrumentValue"]], token: Instrument) -> "InstrumentValue": + def find_by_token( + values: typing.Sequence[typing.Optional["InstrumentValue"]], token: Instrument + ) -> "InstrumentValue": return InstrumentValue.find_by_symbol(values, token.symbol) @staticmethod - def changes(before: typing.Sequence["InstrumentValue"], after: typing.Sequence["InstrumentValue"]) -> typing.Sequence["InstrumentValue"]: + def changes( + before: typing.Sequence["InstrumentValue"], + after: typing.Sequence["InstrumentValue"], + ) -> typing.Sequence["InstrumentValue"]: changes: typing.List[InstrumentValue] = [] for before_balance in before: after_balance = InstrumentValue.find_by_token(after, before_balance.token) - result = InstrumentValue(before_balance.token, after_balance.value - before_balance.value) + result = InstrumentValue( + before_balance.token, after_balance.value - before_balance.value + ) changes += [result] return changes @@ -114,19 +143,23 @@ class InstrumentValue: def __add__(self, token_value_to_add: "InstrumentValue") -> "InstrumentValue": if self.token != token_value_to_add.token: raise Exception( - f"Cannot add InstrumentValues from different tokens ({self.token} and {token_value_to_add.token}).") + f"Cannot add InstrumentValues from different tokens ({self.token} and {token_value_to_add.token})." + ) return InstrumentValue(self.token, self.value + token_value_to_add.value) def __sub__(self, token_value_to_subtract: "InstrumentValue") -> "InstrumentValue": if self.token != token_value_to_subtract.token: raise Exception( - f"Cannot subtract InstrumentValues from different tokens ({self.token} and {token_value_to_subtract.token}).") + f"Cannot subtract InstrumentValues from different tokens ({self.token} and {token_value_to_subtract.token})." + ) return InstrumentValue(self.token, self.value - token_value_to_subtract.value) def __mul__(self, token_value_to_multiply: "InstrumentValue") -> "InstrumentValue": # Multiplying by another InstrumentValue is assumed to be a token value multiplied by a token price. # The result should be denominated in the currency of the price. - return InstrumentValue(token_value_to_multiply.token, self.value * token_value_to_multiply.value) + return InstrumentValue( + token_value_to_multiply.token, self.value * token_value_to_multiply.value + ) def __lt__(self, other: typing.Any) -> bool: if isinstance(other, numbers.Number): @@ -137,7 +170,8 @@ class InstrumentValue: if self.token != other.token: raise Exception( - f"Cannot compare token values when one token is {self.token.symbol} and the other is {other.token.symbol}.") + f"Cannot compare token values when one token is {self.token.symbol} and the other is {other.token.symbol}." + ) return self.value < other.value def __gt__(self, other: typing.Any) -> bool: @@ -149,11 +183,16 @@ class InstrumentValue: if self.token != other.token: raise Exception( - f"Cannot compare token values when one token is {self.token.symbol} and the other is {other.token.symbol}.") + f"Cannot compare token values when one token is {self.token.symbol} and the other is {other.token.symbol}." + ) return self.value > other.value def __eq__(self, other: typing.Any) -> bool: - if isinstance(other, InstrumentValue) and self.token == other.token and self.value == other.value: + if ( + isinstance(other, InstrumentValue) + and self.token == other.token + and self.value == other.value + ): return True return False diff --git a/mango/inventory.py b/mango/inventory.py index 0100b5a..686aafd 100644 --- a/mango/inventory.py +++ b/mango/inventory.py @@ -38,7 +38,14 @@ from .watcher import Watcher # This class details inventory of a crypto account for a market. # class Inventory: - def __init__(self, inventory_source: InventorySource, liquidity_incentives: InstrumentValue, available_collateral: InstrumentValue, base: InstrumentValue, quote: InstrumentValue) -> None: + def __init__( + self, + inventory_source: InventorySource, + liquidity_incentives: InstrumentValue, + available_collateral: InstrumentValue, + base: InstrumentValue, + quote: InstrumentValue, + ) -> None: self._logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.inventory_source: InventorySource = inventory_source self.available_collateral: InstrumentValue = available_collateral @@ -61,16 +68,31 @@ class Inventory: class SpotInventoryAccountWatcher: - def __init__(self, market: Market, account_watcher: Watcher[Account], group_watcher: Watcher[Group], all_open_orders_watchers: typing.Sequence[Watcher[OpenOrders]], cache_watcher: Watcher[Cache]): + def __init__( + self, + market: Market, + account_watcher: Watcher[Account], + group_watcher: Watcher[Group], + all_open_orders_watchers: typing.Sequence[Watcher[OpenOrders]], + cache_watcher: Watcher[Cache], + ): self.account_watcher: Watcher[Account] = account_watcher self.group_watcher: Watcher[Group] = group_watcher - self.all_open_orders_watchers: typing.Sequence[Watcher[OpenOrders]] = all_open_orders_watchers + self.all_open_orders_watchers: typing.Sequence[ + Watcher[OpenOrders] + ] = all_open_orders_watchers self.cache_watcher: Watcher[Cache] = cache_watcher account: Account = account_watcher.latest - self.spot_account_index: int = group_watcher.latest.slot_by_spot_market_address(market.address).index - base_value = InstrumentValue.find_by_symbol(account.net_values, market.base.symbol) + self.spot_account_index: int = group_watcher.latest.slot_by_spot_market_address( + market.address + ).index + base_value = InstrumentValue.find_by_symbol( + account.net_values, market.base.symbol + ) self.base_index: int = account.net_values_by_index.index(base_value) - quote_value = InstrumentValue.find_by_symbol(account.net_values, market.quote.symbol) + quote_value = InstrumentValue.find_by_symbol( + account.net_values, market.quote.symbol + ) self.quote_index: int = account.net_values_by_index.index(quote_value) self.collateral_calculator: CollateralCalculator = SpotCollateralCalculator() @@ -85,31 +107,53 @@ class SpotInventoryAccountWatcher: mngo_accrued: InstrumentValue = InstrumentValue(mngo, Decimal(0)) all_open_orders: typing.Dict[str, OpenOrders] = { - str(oo_watcher.latest.address): oo_watcher.latest for oo_watcher in self.all_open_orders_watchers} + str(oo_watcher.latest.address): oo_watcher.latest + for oo_watcher in self.all_open_orders_watchers + } available_collateral: InstrumentValue = self.collateral_calculator.calculate( - account, all_open_orders, group, cache) + account, all_open_orders, group, cache + ) base_value = account.net_values_by_index[self.base_index] if base_value is None: raise Exception( - f"Could not find net assets in account {account.address} at index {self.base_index}.") + f"Could not find net assets in account {account.address} at index {self.base_index}." + ) quote_value = account.net_values_by_index[self.quote_index] if quote_value is None: raise Exception( - f"Could not find net assets in account {account.address} at index {self.quote_index}.") + f"Could not find net assets in account {account.address} at index {self.quote_index}." + ) - return Inventory(InventorySource.ACCOUNT, mngo_accrued, available_collateral, base_value, quote_value) + return Inventory( + InventorySource.ACCOUNT, + mngo_accrued, + available_collateral, + base_value, + quote_value, + ) class PerpInventoryAccountWatcher: - def __init__(self, market: PerpMarket, account_watcher: Watcher[Account], group_watcher: Watcher[Group], cache_watcher: Watcher[Cache], group: Group): + def __init__( + self, + market: PerpMarket, + account_watcher: Watcher[Account], + group_watcher: Watcher[Group], + cache_watcher: Watcher[Cache], + group: Group, + ): self.market: PerpMarket = market self.account_watcher: Watcher[Account] = account_watcher self.group_watcher: Watcher[Group] = group_watcher self.cache_watcher: Watcher[Cache] = cache_watcher - self.perp_account_index: int = group.slot_by_perp_market_address(market.address).index + self.perp_account_index: int = group.slot_by_perp_market_address( + market.address + ).index account: Account = account_watcher.latest - quote_value = InstrumentValue.find_by_symbol(account.net_values, market.quote.symbol) + quote_value = InstrumentValue.find_by_symbol( + account.net_values, market.quote.symbol + ) self.quote_index: int = account.net_values_by_index.index(quote_value) self.collateral_calculator: CollateralCalculator = PerpCollateralCalculator() @@ -121,9 +165,12 @@ class PerpInventoryAccountWatcher: perp_account = account.perp_accounts_by_index[self.perp_account_index] if perp_account is None: raise Exception( - f"Could not find perp account for {self.market.symbol} in account {account.address} at index {self.perp_account_index}.") + f"Could not find perp account for {self.market.symbol} in account {account.address} at index {self.perp_account_index}." + ) - available_collateral: InstrumentValue = self.collateral_calculator.calculate(account, {}, group, cache) + available_collateral: InstrumentValue = self.collateral_calculator.calculate( + account, {}, group, cache + ) base_lots = perp_account.base_position base_value = self.market.lot_size_converter.base_size_lots_to_number(base_lots) @@ -131,4 +178,10 @@ class PerpInventoryAccountWatcher: base_token_value = InstrumentValue(Token.ensure(self.market.base), base_value) quote_token_value = account.shared_quote.net_value - return Inventory(InventorySource.ACCOUNT, perp_account.mngo_accrued, available_collateral, base_token_value, quote_token_value) + return Inventory( + InventorySource.ACCOUNT, + perp_account.mngo_accrued, + available_collateral, + base_token_value, + quote_token_value, + ) diff --git a/mango/layouts/layouts.py b/mango/layouts/layouts.py index 22f72b1..32944f9 100644 --- a/mango/layouts/layouts.py +++ b/mango/layouts/layouts.py @@ -49,10 +49,13 @@ from solana.publickey import PublicKey # A simple construct `Adapter` that lets us use `Decimal`s directly in our structs. # if typing.TYPE_CHECKING: + class DecimalAdapter(construct.Adapter[Decimal, int, typing.Any, typing.Any]): def __init__(self, size: int = 8) -> None: pass + else: + class DecimalAdapter(construct.Adapter): def __init__(self, size: int = 8) -> None: super().__init__(construct.BytesInteger(size, swapped=True)) @@ -86,10 +89,13 @@ else: # divisor so the mid-point of the whole sequence of bits is the fixed point.) # if typing.TYPE_CHECKING: + class FloatAdapter(construct.Adapter[Decimal, int, typing.Any, typing.Any]): def __init__(self, size: int = 16) -> None: pass + else: + class FloatAdapter(construct.Adapter): def __init__(self, size: int = 16) -> None: self.size = size @@ -102,7 +108,7 @@ else: fixed_point = bit_size / 2 # So our divisor is 2 to the power of the fixed point - self.divisor = Decimal(2 ** fixed_point) + self.divisor = Decimal(2**fixed_point) def _decode(self, obj: int, context: typing.Any, path: typing.Any) -> Decimal: return Decimal(obj) / self.divisor @@ -116,10 +122,13 @@ else: # Another simple `Decimal` `Adapter` but this one specifically works with signed decimals. # if typing.TYPE_CHECKING: + class SignedDecimalAdapter(construct.Adapter[Decimal, int, typing.Any, typing.Any]): def __init__(self, size: int = 8) -> None: pass + else: + class SignedDecimalAdapter(construct.Adapter): def __init__(self, size: int = 8) -> None: super().__init__(construct.BytesInteger(size, signed=True, swapped=True)) @@ -137,20 +146,27 @@ else: # A simple construct `Adapter` that lets us use `PublicKey`s directly in our structs. # if typing.TYPE_CHECKING: + class PublicKeyAdapter(construct.Adapter[PublicKey, bytes, typing.Any, typing.Any]): def __init__(self) -> None: pass + else: + class PublicKeyAdapter(construct.Adapter): def __init__(self) -> None: super().__init__(construct.Bytes(32)) - def _decode(self, obj: bytes, context: typing.Any, path: typing.Any) -> typing.Optional[PublicKey]: + def _decode( + self, obj: bytes, context: typing.Any, path: typing.Any + ) -> typing.Optional[PublicKey]: if (obj is None) or (obj == bytes([0] * 32)): return None return PublicKey(obj) - def _encode(self, obj: PublicKey, context: typing.Any, path: typing.Any) -> bytes: + def _encode( + self, obj: PublicKey, context: typing.Any, path: typing.Any + ) -> bytes: return bytes(obj) @@ -159,18 +175,27 @@ else: # A simple construct `Adapter` that lets us load `datetime`s directly in our structs. # if typing.TYPE_CHECKING: - class DatetimeAdapter(construct.Adapter[datetime.datetime, int, typing.Any, typing.Any]): + + class DatetimeAdapter( + construct.Adapter[datetime.datetime, int, typing.Any, typing.Any] + ): def __init__(self) -> None: pass + else: + class DatetimeAdapter(construct.Adapter): def __init__(self) -> None: super().__init__(construct.BytesInteger(8, swapped=True)) - def _decode(self, obj: int, context: typing.Any, path: typing.Any) -> datetime.datetime: + def _decode( + self, obj: int, context: typing.Any, path: typing.Any + ) -> datetime.datetime: return datetime.datetime.fromtimestamp(obj, tz=datetime.timezone.utc) - def _encode(self, obj: datetime.datetime, context: typing.Any, path: typing.Any) -> int: + def _encode( + self, obj: datetime.datetime, context: typing.Any, path: typing.Any + ) -> int: return int(obj.timestamp()) @@ -183,21 +208,26 @@ else: # integer part and the last 6 bytes are the fractional part. # if typing.TYPE_CHECKING: + class FloatI80F48Adapter(construct.Adapter[Decimal, int, typing.Any, typing.Any]): def __init__(self) -> None: pass + else: + class FloatI80F48Adapter(construct.Adapter): def __init__(self) -> None: self.size = 16 - super().__init__(construct.BytesInteger(self.size, signed=True, swapped=True)) + super().__init__( + construct.BytesInteger(self.size, signed=True, swapped=True) + ) # For our string of bits, our 'fixed point' is between the 10th byte and 11th byte. We want # the last 6 bytes to be fractional, so: fixed_point_in_bits = 8 * 6 # So our divisor is 2 to the power of the fixed point - self.divisor = Decimal(2 ** fixed_point_in_bits) + self.divisor = Decimal(2**fixed_point_in_bits) def _decode(self, obj: int, context: typing.Any, path: typing.Any) -> Decimal: # How many decimal places precision should we allow for an I80F48? TypeScript seems to have @@ -207,7 +237,9 @@ else: # than 20, not raise an exception when we have a sufficiently rounded number already. value: Decimal = Decimal(obj) divided: Decimal = value / self.divisor - return divided.quantize(Decimal('.00000000000000000001'), context=DecimalContext(prec=100)) + return divided.quantize( + Decimal(".00000000000000000001"), context=DecimalContext(prec=100) + ) def _encode(self, obj: Decimal, context: typing.Any, path: typing.Any) -> int: return int(obj) @@ -226,28 +258,37 @@ else: # dictionary. # if typing.TYPE_CHECKING: - class BookPriceAdapter(construct.Adapter[typing.Dict[str, Decimal], bytes, typing.Any, typing.Any]): + + class BookPriceAdapter( + construct.Adapter[typing.Dict[str, Decimal], bytes, typing.Any, typing.Any] + ): def __init__(self) -> None: pass + else: + class BookPriceAdapter(construct.Adapter): def __init__(self) -> None: super().__init__(construct.Bytes(16)) - def _decode(self, obj: bytes, context: typing.Any, path: typing.Any) -> typing.Dict[str, Decimal]: - order_id = Decimal(int.from_bytes(obj, 'little', signed=False)) + def _decode( + self, obj: bytes, context: typing.Any, path: typing.Any + ) -> typing.Dict[str, Decimal]: + order_id = Decimal(int.from_bytes(obj, "little", signed=False)) low_order = obj[:8] high_order = obj[8:] - sequence_number = Decimal(int.from_bytes(low_order, 'little', signed=False)) - price = Decimal(int.from_bytes(high_order, 'little', signed=False)) + sequence_number = Decimal(int.from_bytes(low_order, "little", signed=False)) + price = Decimal(int.from_bytes(high_order, "little", signed=False)) return { "order_id": order_id, "price": price, - "sequence_number": sequence_number + "sequence_number": sequence_number, } - def _encode(self, obj: typing.Dict[str, Decimal], context: typing.Any, path: typing.Any) -> bytes: + def _encode( + self, obj: typing.Dict[str, Decimal], context: typing.Any, path: typing.Any + ) -> bytes: # Not done yet raise NotImplementedError() @@ -262,15 +303,22 @@ _NODE_SIZE = 88 if typing.TYPE_CHECKING: - class OrderBookNodeAdapter(construct.Adapter[typing.Any, typing.Any, typing.Any, typing.Any]): + + class OrderBookNodeAdapter( + construct.Adapter[typing.Any, typing.Any, typing.Any, typing.Any] + ): def __init__(self) -> None: pass + else: + class OrderBookNodeAdapter(construct.Adapter): def __init__(self) -> None: super().__init__(construct.Bytes(_NODE_SIZE)) - def _decode(self, obj: bytes, context: typing.Any, path: typing.Any) -> typing.Any: + def _decode( + self, obj: bytes, context: typing.Any, path: typing.Any + ) -> typing.Any: any_node = ANY_NODE.parse(obj) if any_node.tag == Decimal(0): return UNINITIALIZED_BOOK_NODE.parse(obj) @@ -285,7 +333,9 @@ else: raise Exception(f"Unknown node type tag: {any_node.tag}") - def _encode(self, obj: typing.Any, context: typing.Any, path: typing.Any) -> typing.Any: + def _encode( + self, obj: typing.Any, context: typing.Any, path: typing.Any + ) -> typing.Any: # Not done yet raise NotImplementedError() @@ -319,7 +369,7 @@ ACCOUNT_FLAGS = construct.BitsSwapped( "bids" / construct.Flag, "asks" / construct.Flag, "disabled" / construct.Flag, - construct.Padding(7 * 8) + construct.Padding(7 * 8), ) ) @@ -329,7 +379,7 @@ TOKEN_ACCOUNT = construct.Struct( "mint" / PublicKeyAdapter(), "owner" / PublicKeyAdapter(), "amount" / DecimalAdapter(), - "padding" / construct.Padding(93) + "padding" / construct.Padding(93), ) @@ -352,7 +402,7 @@ OPEN_ORDERS = construct.Struct( "orders" / construct.Array(128, DecimalAdapter(16)), "client_ids" / construct.Array(128, DecimalAdapter()), "referrer_rebate_accrued" / DecimalAdapter(), - "padding" / construct.Padding(7) + "padding" / construct.Padding(7), ) # Mints and airdrops tokens from a faucet. @@ -371,7 +421,7 @@ OPEN_ORDERS = construct.Struct( # /// 5. `[optional/signer]` Admin Account FAUCET_AIRDROP = construct.Struct( "variant" / construct.Const(1, construct.BytesInteger(1, swapped=True)), - "quantity" / DecimalAdapter() + "quantity" / DecimalAdapter(), ) @@ -383,8 +433,18 @@ MAX_BOOK_NODES: int = 1024 MAX_ORDERS: int = 32 MAX_PERP_OPEN_ORDERS: int = 64 -DATA_TYPE = construct.Enum(construct.Int8ul, Group=0, Account=1, RootBank=2, - NodeBank=3, PerpMarket=4, Bids=5, Asks=6, Cache=7, EventQueue=8) +DATA_TYPE = construct.Enum( + construct.Int8ul, + Group=0, + Account=1, + RootBank=2, + NodeBank=3, + PerpMarket=4, + Bids=5, + Asks=6, + Cache=7, + EventQueue=8, +) # # ๐Ÿฅญ METADATA @@ -405,7 +465,7 @@ METADATA = construct.Struct( "data_type" / DATA_TYPE, "version" / DecimalAdapter(1), "is_initialized" / DecimalAdapter(1), - "padding" / construct.Padding(5) + "padding" / construct.Padding(5), ) @@ -426,7 +486,7 @@ TOKEN_INFO = construct.Struct( "mint" / PublicKeyAdapter(), "root_bank" / PublicKeyAdapter(), "decimals" / DecimalAdapter(1), - "padding" / construct.Padding(7) + "padding" / construct.Padding(7), ) @@ -451,7 +511,7 @@ SPOT_MARKET_INFO = construct.Struct( "init_asset_weight" / FloatI80F48Adapter(), "maint_liab_weight" / FloatI80F48Adapter(), "init_liab_weight" / FloatI80F48Adapter(), - "liquidation_fee" / FloatI80F48Adapter() + "liquidation_fee" / FloatI80F48Adapter(), ) # # ๐Ÿฅญ PERP_MARKET_INFO @@ -544,7 +604,7 @@ GROUP = construct.Struct( "referral_surcharge_centibps" / DecimalAdapter(4), "referral_share_centibps" / DecimalAdapter(4), "referral_mngo_required" / DecimalAdapter(), - construct.Padding(8) + construct.Padding(8), ) # # ๐Ÿฅญ ROOT_BANK @@ -576,14 +636,12 @@ ROOT_BANK = construct.Struct( "optimal_util" / FloatI80F48Adapter(), "optimal_rate" / FloatI80F48Adapter(), "max_rate" / FloatI80F48Adapter(), - "num_node_banks" / DecimalAdapter(), "node_banks" / construct.Array(MAX_NODE_BANKS, PublicKeyAdapter()), "deposit_index" / FloatI80F48Adapter(), "borrow_index" / FloatI80F48Adapter(), "last_updated" / DatetimeAdapter(), - - construct.Padding(64) + construct.Padding(64), ) # # ๐Ÿฅญ NODE_BANK @@ -604,7 +662,7 @@ NODE_BANK = construct.Struct( "meta_data" / METADATA, "deposits" / FloatI80F48Adapter(), "borrows" / FloatI80F48Adapter(), - "vault" / PublicKeyAdapter() + "vault" / PublicKeyAdapter(), ) # # ๐Ÿฅญ PERP_ACCOUNT @@ -634,16 +692,12 @@ NODE_BANK = construct.Struct( PERP_ACCOUNT = construct.Struct( "base_position" / SignedDecimalAdapter(), "quote_position" / FloatI80F48Adapter(), - "long_settled_funding" / FloatI80F48Adapter(), "short_settled_funding" / FloatI80F48Adapter(), - "bids_quantity" / SignedDecimalAdapter(), "asks_quantity" / SignedDecimalAdapter(), - "taker_base" / SignedDecimalAdapter(), "taker_quote" / SignedDecimalAdapter(), - "mngo_accrued" / DecimalAdapter(), ) @@ -724,7 +778,7 @@ MANGO_ACCOUNT = construct.Struct( "advanced_orders" / PublicKeyAdapter(), "not_upgradable" / construct.Flag, "delegate" / PublicKeyAdapter(), - construct.Padding(5) + construct.Padding(5), ) # # ๐Ÿฅญ LIQUIDITY_MINING_INFO @@ -755,16 +809,11 @@ MANGO_ACCOUNT = construct.Struct( # ``` LIQUIDITY_MINING_INFO = construct.Struct( "rate" / FloatI80F48Adapter(), - "max_depth_bps" / FloatI80F48Adapter(), - "period_start" / DatetimeAdapter(), - "target_period_length" / DecimalAdapter(), - "mngo_left" / DecimalAdapter(), - - "mngo_per_period" / DecimalAdapter() + "mngo_per_period" / DecimalAdapter(), ) @@ -810,19 +859,14 @@ PERP_MARKET = construct.Struct( "event_queue" / PublicKeyAdapter(), "quote_lot_size" / SignedDecimalAdapter(), "base_lot_size" / SignedDecimalAdapter(), - "long_funding" / FloatI80F48Adapter(), "short_funding" / FloatI80F48Adapter(), - "open_interest" / SignedDecimalAdapter(), - "last_updated" / DatetimeAdapter(), "seq_num" / DecimalAdapter(), "fees_accrued" / FloatI80F48Adapter(), - "liquidity_mining_info" / LIQUIDITY_MINING_INFO, - - "mngo_vault" / PublicKeyAdapter() + "mngo_vault" / PublicKeyAdapter(), ) @@ -839,11 +883,12 @@ PERP_MARKET = construct.Struct( # } # ``` ANY_NODE = construct.Struct( - "tag" / DecimalAdapter(4), - "data" / construct.Bytes(_NODE_SIZE - 4) + "tag" / DecimalAdapter(4), "data" / construct.Bytes(_NODE_SIZE - 4) ) if ANY_NODE.sizeof() != _NODE_SIZE: - raise Exception(f"Incorrect size for ANY_NODE: expected: {_NODE_SIZE}, got: {ANY_NODE.sizeof()}") + raise Exception( + f"Incorrect size for ANY_NODE: expected: {_NODE_SIZE}, got: {ANY_NODE.sizeof()}" + ) # # ๐Ÿฅญ UNINITIALIZED_BOOK_NODE @@ -853,11 +898,12 @@ if ANY_NODE.sizeof() != _NODE_SIZE: UNINITIALIZED_BOOK_NODE = construct.Struct( "type_name" / construct.Computed(lambda _: "uninitialized"), "tag" / construct.Const(Decimal(0), DecimalAdapter(4)), - "data" / construct.Bytes(_NODE_SIZE - 4) + "data" / construct.Bytes(_NODE_SIZE - 4), ) if UNINITIALIZED_BOOK_NODE.sizeof() != _NODE_SIZE: raise Exception( - f"Incorrect size for UNINITIALIZED_BOOK_NODE: expected: {_NODE_SIZE}, got: {UNINITIALIZED_BOOK_NODE.sizeof()}") + f"Incorrect size for UNINITIALIZED_BOOK_NODE: expected: {_NODE_SIZE}, got: {UNINITIALIZED_BOOK_NODE.sizeof()}" + ) # # ๐Ÿฅญ INNER_BOOK_NODE # @@ -880,11 +926,12 @@ INNER_BOOK_NODE = construct.Struct( "prefix_len" / DecimalAdapter(4), "key" / DecimalAdapter(16), "children" / construct.Array(2, DecimalAdapter(4)), - "padding" / construct.Padding(_NODE_SIZE - 32) + "padding" / construct.Padding(_NODE_SIZE - 32), ) if INNER_BOOK_NODE.sizeof() != _NODE_SIZE: raise Exception( - f"Incorrect size for INNER_BOOK_NODE: expected: {_NODE_SIZE}, got: {INNER_BOOK_NODE.sizeof()}") + f"Incorrect size for INNER_BOOK_NODE: expected: {_NODE_SIZE}, got: {INNER_BOOK_NODE.sizeof()}" + ) # # ๐Ÿฅญ LEAF_BOOK_NODE # @@ -921,13 +968,13 @@ LEAF_BOOK_NODE = construct.Struct( # In units of lot size "quantity" / DecimalAdapter(), "client_order_id" / DecimalAdapter(), - "best_initial" / SignedDecimalAdapter(), - "timestamp" / DatetimeAdapter() + "timestamp" / DatetimeAdapter(), ) if LEAF_BOOK_NODE.sizeof() != _NODE_SIZE: raise Exception( - f"Incorrect size for LEAF_BOOK_NODE: expected: {_NODE_SIZE}, got: {LEAF_BOOK_NODE.sizeof()}") + f"Incorrect size for LEAF_BOOK_NODE: expected: {_NODE_SIZE}, got: {LEAF_BOOK_NODE.sizeof()}" + ) # # ๐Ÿฅญ FREE_BOOK_NODE # @@ -945,11 +992,12 @@ FREE_BOOK_NODE = construct.Struct( "type_name" / construct.Computed(lambda _: "free"), "tag" / construct.Const(Decimal(3), DecimalAdapter(4)), "next" / DecimalAdapter(4), - "padding" / construct.Padding(_NODE_SIZE - 8) + "padding" / construct.Padding(_NODE_SIZE - 8), ) if FREE_BOOK_NODE.sizeof() != _NODE_SIZE: raise Exception( - f"Incorrect size for FREE_BOOK_NODE: expected: {_NODE_SIZE}, got: {FREE_BOOK_NODE.sizeof()}") + f"Incorrect size for FREE_BOOK_NODE: expected: {_NODE_SIZE}, got: {FREE_BOOK_NODE.sizeof()}" + ) # # ๐Ÿฅญ LAST_FREE_BOOK_NODE @@ -960,11 +1008,12 @@ LAST_FREE_BOOK_NODE = construct.Struct( "type_name" / construct.Computed(lambda _: "last_free"), "tag" / construct.Const(Decimal(4), DecimalAdapter(4)), "next" / DecimalAdapter(4), - "padding" / construct.Padding(_NODE_SIZE - 8) + "padding" / construct.Padding(_NODE_SIZE - 8), ) if LAST_FREE_BOOK_NODE.sizeof() != _NODE_SIZE: raise Exception( - f"Incorrect size for LAST_FREE_BOOK_NODE: expected: {_NODE_SIZE}, got: {LAST_FREE_BOOK_NODE.sizeof()}") + f"Incorrect size for LAST_FREE_BOOK_NODE: expected: {_NODE_SIZE}, got: {LAST_FREE_BOOK_NODE.sizeof()}" + ) # # ๐Ÿฅญ ORDERBOOK_SIDE @@ -993,7 +1042,7 @@ ORDERBOOK_SIDE = construct.Struct( "free_list_head" / DecimalAdapter(4), "root_node" / DecimalAdapter(4), "leaf_count" / DecimalAdapter(), - "nodes" / construct.Array(MAX_BOOK_NODES, OrderBookNodeAdapter()) + "nodes" / construct.Array(MAX_BOOK_NODES, OrderBookNodeAdapter()), ) @@ -1034,33 +1083,31 @@ ORDERBOOK_SIDE = construct.Struct( # } # ``` FILL_EVENT = construct.Struct( - "event_type" / construct.Const(b'\x00'), + "event_type" / construct.Const(b"\x00"), "taker_side" / DecimalAdapter(1), "maker_slot" / DecimalAdapter(1), "maker_out" / construct.Flag, construct.Padding(4), "timestamp" / DatetimeAdapter(), "seq_num" / DecimalAdapter(), - "maker" / PublicKeyAdapter(), "maker_order_id" / SignedDecimalAdapter(16), "maker_client_order_id" / DecimalAdapter(), "maker_fee" / FloatI80F48Adapter(), - "best_initial" / SignedDecimalAdapter(), "maker_timestamp" / DatetimeAdapter(), - "taker" / PublicKeyAdapter(), "taker_order_id" / SignedDecimalAdapter(16), "taker_client_order_id" / DecimalAdapter(), "taker_fee" / FloatI80F48Adapter(), - "price" / SignedDecimalAdapter(), - "quantity" / SignedDecimalAdapter() + "quantity" / SignedDecimalAdapter(), ) _EVENT_SIZE = 200 if FILL_EVENT.sizeof() != _EVENT_SIZE: - raise Exception(f"Fill event size is {FILL_EVENT.sizeof()} when it should be {_EVENT_SIZE}.") + raise Exception( + f"Fill event size is {FILL_EVENT.sizeof()} when it should be {_EVENT_SIZE}." + ) # # ๐Ÿฅญ OUT_EVENT # @@ -1081,7 +1128,7 @@ if FILL_EVENT.sizeof() != _EVENT_SIZE: # } # ``` OUT_EVENT = construct.Struct( - "event_type" / construct.Const(b'\x01'), + "event_type" / construct.Const(b"\x01"), "side" / DecimalAdapter(1), "slot" / DecimalAdapter(1), construct.Padding(5), @@ -1089,10 +1136,12 @@ OUT_EVENT = construct.Struct( "seq_num" / DecimalAdapter(), "owner" / PublicKeyAdapter(), "quantity" / SignedDecimalAdapter(), - construct.Padding(_EVENT_SIZE - 64) + construct.Padding(_EVENT_SIZE - 64), ) if OUT_EVENT.sizeof() != _EVENT_SIZE: - raise Exception(f"Out event size is {OUT_EVENT.sizeof()} when it should be {_EVENT_SIZE}.") + raise Exception( + f"Out event size is {OUT_EVENT.sizeof()} when it should be {_EVENT_SIZE}." + ) # # ๐Ÿฅญ LIQUIDATE_EVENT @@ -1116,7 +1165,7 @@ if OUT_EVENT.sizeof() != _EVENT_SIZE: # } # ``` LIQUIDATE_EVENT = construct.Struct( - "event_type" / construct.Const(b'\x02'), + "event_type" / construct.Const(b"\x02"), construct.Padding(7), "timestamp" / DatetimeAdapter(), "seq_num" / DecimalAdapter(), @@ -1125,20 +1174,24 @@ LIQUIDATE_EVENT = construct.Struct( "price" / FloatI80F48Adapter(), "quantity" / SignedDecimalAdapter(), "liquidation_fee" / FloatI80F48Adapter(), - construct.Padding(_EVENT_SIZE - 128) + construct.Padding(_EVENT_SIZE - 128), ) if LIQUIDATE_EVENT.sizeof() != _EVENT_SIZE: - raise Exception(f"Liquidate event size is {LIQUIDATE_EVENT.sizeof()} when it should be {_EVENT_SIZE}.") + raise Exception( + f"Liquidate event size is {LIQUIDATE_EVENT.sizeof()} when it should be {_EVENT_SIZE}." + ) UNKNOWN_EVENT = construct.Struct( "event_type" / construct.Bytes(1), construct.Padding(7), "owner" / PublicKeyAdapter(), - construct.Padding(_EVENT_SIZE - 40) + construct.Padding(_EVENT_SIZE - 40), ) if UNKNOWN_EVENT.sizeof() != _EVENT_SIZE: - raise Exception(f"Unknown event size is {UNKNOWN_EVENT.sizeof()} when it should be {_EVENT_SIZE}.") + raise Exception( + f"Unknown event size is {UNKNOWN_EVENT.sizeof()} when it should be {_EVENT_SIZE}." + ) # # ๐Ÿฅญ PERP_EVENT_QUEUE @@ -1169,7 +1222,10 @@ PERP_EVENT_QUEUE = construct.Struct( "seq_num" / DecimalAdapter(), # "maker_fee" / FloatI80F48Adapter(), # "taker_fee" / FloatI80F48Adapter(), - "events" / construct.GreedyRange(construct.Select(FILL_EVENT, OUT_EVENT, LIQUIDATE_EVENT, UNKNOWN_EVENT)) + "events" + / construct.GreedyRange( + construct.Select(FILL_EVENT, OUT_EVENT, LIQUIDATE_EVENT, UNKNOWN_EVENT) + ), ) # # ๐Ÿฅญ SERUM_EVENT_QUEUE @@ -1183,7 +1239,9 @@ SERUM_EVENT_FLAGS = construct.BitsSwapped( "out" / construct.Flag, "bid" / construct.Flag, "maker" / construct.Flag, - construct.Padding(4))) + construct.Padding(4), + ) +) SERUM_EVENT = construct.Struct( "event_flags" / SERUM_EVENT_FLAGS, @@ -1207,32 +1265,31 @@ SERUM_EVENT_QUEUE = construct.Struct( construct.Padding(4), "next_seq_num" / DecimalAdapter(4), construct.Padding(4), - "events" / construct.GreedyRange(SERUM_EVENT) + "events" / construct.GreedyRange(SERUM_EVENT), ) PRICE_CACHE = construct.Struct( - "price" / FloatI80F48Adapter(), - "last_update" / DatetimeAdapter() + "price" / FloatI80F48Adapter(), "last_update" / DatetimeAdapter() ) ROOT_BANK_CACHE = construct.Struct( "deposit_index" / FloatI80F48Adapter(), "borrow_index" / FloatI80F48Adapter(), - "last_update" / DatetimeAdapter() + "last_update" / DatetimeAdapter(), ) PERP_MARKET_CACHE = construct.Struct( "long_funding" / FloatI80F48Adapter(), "short_funding" / FloatI80F48Adapter(), - "last_update" / DatetimeAdapter() + "last_update" / DatetimeAdapter(), ) CACHE = construct.Struct( "meta_data" / METADATA, "price_cache" / construct.Array(MAX_PAIRS, PRICE_CACHE), "root_bank_cache" / construct.Array(MAX_TOKENS, ROOT_BANK_CACHE), - "perp_market_cache" / construct.Array(MAX_PAIRS, PERP_MARKET_CACHE) + "perp_market_cache" / construct.Array(MAX_PAIRS, PERP_MARKET_CACHE), ) @@ -1254,7 +1311,7 @@ MANGO_INSTRUCTION_VARIANT_FINDER = construct.Struct( SERUM_INSTRUCTION_VARIANT_FINDER = construct.Struct( "version" / construct.BytesInteger(1, swapped=True), - "variant" / construct.BytesInteger(4, swapped=True) + "variant" / construct.BytesInteger(4, swapped=True), ) @@ -1276,8 +1333,7 @@ SERUM_INSTRUCTION_VARIANT_FINDER = construct.Struct( # }, DEPOSIT = construct.Struct( "variant" / construct.Const(2, construct.BytesInteger(4, swapped=True)), - - "quantity" / DecimalAdapter() + "quantity" / DecimalAdapter(), ) @@ -1303,9 +1359,8 @@ DEPOSIT = construct.Struct( # }, WITHDRAW = construct.Struct( "variant" / construct.Const(3, construct.BytesInteger(4, swapped=True)), - "quantity" / DecimalAdapter(), - "allow_borrow" / DecimalAdapter(1) + "allow_borrow" / DecimalAdapter(1), ) @@ -1341,15 +1396,14 @@ WITHDRAW = construct.Struct( # })), PLACE_SPOT_ORDER = construct.Struct( "variant" / construct.Const(9, construct.BytesInteger(4, swapped=True)), # 4 - - 'side' / DecimalAdapter(4), # 8 + "side" / DecimalAdapter(4), # 8 "limit_price" / DecimalAdapter(), # 16 - 'max_base_quantity' / DecimalAdapter(), # 24 - 'max_quote_quantity' / DecimalAdapter(), # 32 - 'self_trade_behavior' / DecimalAdapter(4), # 36 - 'order_type' / DecimalAdapter(4), # 40 - 'client_id' / DecimalAdapter(), # 48 - 'limit' / DecimalAdapter(2), # 50 + "max_base_quantity" / DecimalAdapter(), # 24 + "max_quote_quantity" / DecimalAdapter(), # 32 + "self_trade_behavior" / DecimalAdapter(4), # 36 + "order_type" / DecimalAdapter(4), # 40 + "client_id" / DecimalAdapter(), # 48 + "limit" / DecimalAdapter(2), # 50 ) @@ -1365,13 +1419,12 @@ PLACE_SPOT_ORDER = construct.Struct( # /// 7. `[writable]` event_queue_ai - TODO PLACE_PERP_ORDER = construct.Struct( "variant" / construct.Const(12, construct.BytesInteger(4, swapped=True)), - "price" / SignedDecimalAdapter(), "quantity" / SignedDecimalAdapter(), "client_order_id" / DecimalAdapter(), "side" / DecimalAdapter(1), # { buy: 0, sell: 1 } "order_type" / DecimalAdapter(1), # { limit: 0, ioc: 1, postOnly: 2 } - "reduce_only" / construct.Flag + "reduce_only" / construct.Flag, ) @@ -1386,9 +1439,8 @@ PLACE_PERP_ORDER = construct.Struct( # 6. `[writable]` eventQueuePk CANCEL_PERP_ORDER_BY_CLIENT_ID = construct.Struct( "variant" / construct.Const(13, construct.BytesInteger(4, swapped=True)), - "client_order_id" / DecimalAdapter(), - "invalid_id_ok" / construct.Flag + "invalid_id_ok" / construct.Flag, ) @@ -1403,9 +1455,8 @@ CANCEL_PERP_ORDER_BY_CLIENT_ID = construct.Struct( # 6. `[writable]` eventQueuePk CANCEL_PERP_ORDER = construct.Struct( "variant" / construct.Const(14, construct.BytesInteger(4, swapped=True)), - "order_id" / DecimalAdapter(16), - "invalid_id_ok" / construct.Flag + "invalid_id_ok" / construct.Flag, ) @@ -1417,8 +1468,7 @@ CANCEL_PERP_ORDER = construct.Struct( # 3+ `[writable]` mangoAccountPks... CONSUME_EVENTS = construct.Struct( "variant" / construct.Const(15, construct.BytesInteger(4, swapped=True)), - - "limit" / DecimalAdapter() + "limit" / DecimalAdapter(), ) @@ -1458,9 +1508,8 @@ SETTLE_FUNDS = construct.Struct( # }, CANCEL_SPOT_ORDER = construct.Struct( "variant" / construct.Const(20, construct.BytesInteger(4, swapped=True)), - - 'side' / DecimalAdapter(4), - "order_id" / DecimalAdapter(16) + "side" / DecimalAdapter(4), + "order_id" / DecimalAdapter(16), ) @@ -1494,8 +1543,7 @@ REDEEM_MNGO = construct.Struct( # /// 5. `[writable]` asks_ai - Asks acc CANCEL_ALL_PERP_ORDERS = construct.Struct( "variant" / construct.Const(39, construct.BytesInteger(4, swapped=True)), - - "limit" / DecimalAdapter(1) + "limit" / DecimalAdapter(1), ) @@ -1529,15 +1577,14 @@ CANCEL_ALL_PERP_ORDERS = construct.Struct( # })), PLACE_SPOT_ORDER_2 = construct.Struct( "variant" / construct.Const(41, construct.BytesInteger(4, swapped=True)), # 4 - - 'side' / DecimalAdapter(4), # 8 + "side" / DecimalAdapter(4), # 8 "limit_price" / DecimalAdapter(), # 16 - 'max_base_quantity' / DecimalAdapter(), # 24 - 'max_quote_quantity' / DecimalAdapter(), # 32 - 'self_trade_behavior' / DecimalAdapter(4), # 36 - 'order_type' / DecimalAdapter(4), # 40 - 'client_id' / DecimalAdapter(), # 48 - 'limit' / DecimalAdapter(2), # 50 + "max_base_quantity" / DecimalAdapter(), # 24 + "max_quote_quantity" / DecimalAdapter(), # 32 + "self_trade_behavior" / DecimalAdapter(4), # 36 + "order_type" / DecimalAdapter(4), # 40 + "client_id" / DecimalAdapter(), # 48 + "limit" / DecimalAdapter(2), # 50 ) @@ -1563,7 +1610,7 @@ CLOSE_MANGO_ACCOUNT = construct.Struct( # /// 3. `[]` system_prog_ai - System program CREATE_MANGO_ACCOUNT = construct.Struct( "variant" / construct.Const(55, construct.BytesInteger(4, swapped=True)), - "account_num" / DecimalAdapter() + "account_num" / DecimalAdapter(), ) @@ -1632,9 +1679,7 @@ REGISTER_REFERRER_ID = construct.Struct( ) -UNSPECIFIED = construct.Struct( - "variant" / DecimalAdapter(4) -) +UNSPECIFIED = construct.Struct("variant" / DecimalAdapter(4)) InstructionParsersByVariant = { diff --git a/mango/liquidatablereport.py b/mango/liquidatablereport.py index 38e55e7..fef35d9 100644 --- a/mango/liquidatablereport.py +++ b/mango/liquidatablereport.py @@ -30,6 +30,7 @@ from .instrumentvalue import InstrumentValue # A margin account may have a combination of these flag values. # + class LiquidatableState(enum.Flag): UNSET = 0 RIPE = enum.auto() @@ -42,8 +43,16 @@ class LiquidatableState(enum.Flag): # # ๐Ÿฅญ LiquidatableReport class # + class LiquidatableReport: - def __init__(self, group: Group, prices: typing.Sequence[InstrumentValue], account: Account, state: LiquidatableState, worthwhile_threshold: Decimal) -> None: + def __init__( + self, + group: Group, + prices: typing.Sequence[InstrumentValue], + account: Account, + state: LiquidatableState, + worthwhile_threshold: Decimal, + ) -> None: self._logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.group: Group = group self.prices: typing.Sequence[InstrumentValue] = prices @@ -52,5 +61,12 @@ class LiquidatableReport: self.worthwhile_threshold: Decimal = worthwhile_threshold @staticmethod - def build(group: Group, prices: typing.Sequence[InstrumentValue], account: Account, worthwhile_threshold: Decimal) -> "LiquidatableReport": - return LiquidatableReport(group, prices, account, LiquidatableState.UNSET, worthwhile_threshold) + def build( + group: Group, + prices: typing.Sequence[InstrumentValue], + account: Account, + worthwhile_threshold: Decimal, + ) -> "LiquidatableReport": + return LiquidatableReport( + group, prices, account, LiquidatableState.UNSET, worthwhile_threshold + ) diff --git a/mango/liquidationevent.py b/mango/liquidationevent.py index 9974068..f9bad4a 100644 --- a/mango/liquidationevent.py +++ b/mango/liquidationevent.py @@ -24,7 +24,18 @@ from .instrumentvalue import InstrumentValue # # ๐Ÿฅญ LiquidationEvent class # class LiquidationEvent: - def __init__(self, timestamp: datetime.datetime, liquidator_name: str, group_name: str, succeeded: bool, signatures: typing.Sequence[str], wallet_address: PublicKey, account_address: PublicKey, balances_before: typing.Sequence[InstrumentValue], balances_after: typing.Sequence[InstrumentValue]) -> None: + def __init__( + self, + timestamp: datetime.datetime, + liquidator_name: str, + group_name: str, + succeeded: bool, + signatures: typing.Sequence[str], + wallet_address: PublicKey, + account_address: PublicKey, + balances_before: typing.Sequence[InstrumentValue], + balances_after: typing.Sequence[InstrumentValue], + ) -> None: self.timestamp: datetime.datetime = timestamp self.liquidator_name: str = liquidator_name self.group_name: str = group_name @@ -34,11 +45,15 @@ class LiquidationEvent: self.account_address: PublicKey = account_address self.balances_before: typing.Sequence[InstrumentValue] = balances_before self.balances_after: typing.Sequence[InstrumentValue] = balances_after - self.changes: typing.Sequence[InstrumentValue] = InstrumentValue.changes(balances_before, balances_after) + self.changes: typing.Sequence[InstrumentValue] = InstrumentValue.changes( + balances_before, balances_after + ) def __str__(self) -> str: result = "โœ…" if self.succeeded else "โŒ" - changes_text = "\n ".join([f"{change.value:>15,.8f} {change.token.symbol}" for change in self.changes]) + changes_text = "\n ".join( + [f"{change.value:>15,.8f} {change.token.symbol}" for change in self.changes] + ) return f"""ยซ ๐Ÿฅญ Liqudation Event {result} at {self.timestamp} ๐Ÿ’ง Liquidator: {self.liquidator_name} ๐Ÿซ Group: {self.group_name} diff --git a/mango/liquidationprocessor.py b/mango/liquidationprocessor.py index 3184054..e4f4b6f 100644 --- a/mango/liquidationprocessor.py +++ b/mango/liquidationprocessor.py @@ -61,23 +61,35 @@ class LiquidationProcessor: _AGE_ERROR_THRESHOLD = timedelta(minutes=5) _AGE_WARNING_THRESHOLD = timedelta(minutes=2) - def __init__(self, context: Context, name: str, account_liquidator: AccountLiquidator, wallet_balancer: WalletBalancer, worthwhile_threshold: Decimal = Decimal("0.01")) -> None: + def __init__( + self, + context: Context, + name: str, + account_liquidator: AccountLiquidator, + wallet_balancer: WalletBalancer, + worthwhile_threshold: Decimal = Decimal("0.01"), + ) -> None: self._logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.context: Context = context self.name: str = name self.account_liquidator: AccountLiquidator = account_liquidator self.wallet_balancer: WalletBalancer = wallet_balancer self.worthwhile_threshold: Decimal = worthwhile_threshold - self.liquidations: EventSource[LiquidationEvent] = EventSource[LiquidationEvent]() + self.liquidations: EventSource[LiquidationEvent] = EventSource[ + LiquidationEvent + ]() self.ripe_accounts: typing.Optional[typing.Sequence[Account]] = None self.ripe_accounts_updated_at: datetime = datetime.now() self.prices_updated_at: datetime = datetime.now() self.state: LiquidationProcessorState = LiquidationProcessorState.STARTING - self.state_change: EventSource[LiquidationProcessor] = EventSource[LiquidationProcessor]() + self.state_change: EventSource[LiquidationProcessor] = EventSource[ + LiquidationProcessor + ]() def update_accounts(self, ripe_accounts: typing.Sequence[Account]) -> None: self._logger.info( - f"Received {len(ripe_accounts)} ripe ๐Ÿฅญ margin accounts to process - prices last updated {self.prices_updated_at:%Y-%m-%d %H:%M:%S}") + f"Received {len(ripe_accounts)} ripe ๐Ÿฅญ margin accounts to process - prices last updated {self.prices_updated_at:%Y-%m-%d %H:%M:%S}" + ) self._check_update_recency("prices", self.prices_updated_at) self.ripe_accounts = ripe_accounts self.ripe_accounts_updated_at = datetime.now() @@ -85,7 +97,9 @@ class LiquidationProcessor: if self.state == LiquidationProcessorState.STARTING: self.state = LiquidationProcessorState.HEALTHY - def update_prices(self, group: Group, prices: typing.Sequence[InstrumentValue]) -> None: + def update_prices( + self, group: Group, prices: typing.Sequence[InstrumentValue] + ) -> None: started_at = time.time() if self.state == LiquidationProcessorState.STARTING: @@ -97,34 +111,67 @@ class LiquidationProcessor: return self._logger.info( - f"Ripe accounts last updated {self.ripe_accounts_updated_at:%Y-%m-%d %H:%M:%S}") + f"Ripe accounts last updated {self.ripe_accounts_updated_at:%Y-%m-%d %H:%M:%S}" + ) self._check_update_recency("ripe account", self.ripe_accounts_updated_at) report: typing.List[str] = [] updated: typing.List[LiquidatableReport] = [] for account in self.ripe_accounts: - updated += [LiquidatableReport.build(group, prices, account, self.worthwhile_threshold)] + updated += [ + LiquidatableReport.build( + group, prices, account, self.worthwhile_threshold + ) + ] - liquidatable = list(filter(lambda report: report.state & LiquidatableState.LIQUIDATABLE, updated)) - report += [f"Of those {len(updated)} ripe accounts, {len(liquidatable)} are liquidatable."] + liquidatable = list( + filter( + lambda report: report.state & LiquidatableState.LIQUIDATABLE, updated + ) + ) + report += [ + f"Of those {len(updated)} ripe accounts, {len(liquidatable)} are liquidatable." + ] - above_water = list(filter(lambda report: report.state & LiquidatableState.ABOVE_WATER, liquidatable)) - report += [f"Of those {len(liquidatable)} liquidatable margin accounts, {len(above_water)} have assets greater than their liabilities."] + above_water = list( + filter( + lambda report: report.state & LiquidatableState.ABOVE_WATER, + liquidatable, + ) + ) + report += [ + f"Of those {len(liquidatable)} liquidatable margin accounts, {len(above_water)} have assets greater than their liabilities." + ] - worthwhile = list(filter(lambda report: report.state & LiquidatableState.WORTHWHILE, above_water)) - report += [f"Of those {len(above_water)} above water margin accounts, {len(worthwhile)} are worthwhile margin accounts with more than ${self.worthwhile_threshold} net assets."] + worthwhile = list( + filter( + lambda report: report.state & LiquidatableState.WORTHWHILE, above_water + ) + ) + report += [ + f"Of those {len(above_water)} above water margin accounts, {len(worthwhile)} are worthwhile margin accounts with more than ${self.worthwhile_threshold} net assets." + ] report_text = "\n ".join(report) - self._logger.info(f"""Running on {len(self.ripe_accounts)} ripe accounts: - {report_text}""") + self._logger.info( + f"""Running on {len(self.ripe_accounts)} ripe accounts: + {report_text}""" + ) self._liquidate_all(group, prices, worthwhile) self.prices_updated_at = datetime.now() time_taken = time.time() - started_at - self._logger.info(f"Check of all ripe ๐Ÿฅญ accounts complete. Time taken: {time_taken:.2f} seconds.") + self._logger.info( + f"Check of all ripe ๐Ÿฅญ accounts complete. Time taken: {time_taken:.2f} seconds." + ) - def _liquidate_all(self, group: Group, prices: typing.Sequence[InstrumentValue], to_liquidate: typing.Sequence[LiquidatableReport]) -> None: + def _liquidate_all( + self, + group: Group, + prices: typing.Sequence[InstrumentValue], + to_liquidate: typing.Sequence[LiquidatableReport], + ) -> None: to_process = list(to_liquidate) while len(to_process) > 0: # TODO - sort this when LiquidationReport has the proper details for V3. @@ -136,26 +183,36 @@ class LiquidationProcessor: self.account_liquidator.liquidate(highest) self.wallet_balancer.balance(self.context, prices) - updated_account = Account.load(self.context, highest.account.address, group) + updated_account = Account.load( + self.context, highest.account.address, group + ) updated_report = LiquidatableReport.build( - group, prices, updated_account, highest.worthwhile_threshold) + group, prices, updated_account, highest.worthwhile_threshold + ) if not (updated_report.state & LiquidatableState.WORTHWHILE): self._logger.info( - f"Margin account {updated_account.address} has been drained and is no longer worthwhile.") + f"Margin account {updated_account.address} has been drained and is no longer worthwhile." + ) else: self._logger.info( - f"Margin account {updated_account.address} is still worthwhile - putting it back on list.") + f"Margin account {updated_account.address} is still worthwhile - putting it back on list." + ) to_process += [updated_report] except Exception as exception: self._logger.error( - f"[{self.name}] Failed to liquidate account '{highest.account.address}' - {exception}.") + f"[{self.name}] Failed to liquidate account '{highest.account.address}' - {exception}." + ) finally: # highest should always be in to_process, but we're outside the try-except block # so let's be a little paranoid about it. - self._logger.info(f"Liquidatable accounts to process was: {len(to_process)}") + self._logger.info( + f"Liquidatable accounts to process was: {len(to_process)}" + ) if highest in to_process: to_process.remove(highest) - self._logger.info(f"Liquidatable accounts to process is now: {len(to_process)}") + self._logger.info( + f"Liquidatable accounts to process is now: {len(to_process)}" + ) def _check_update_recency(self, name: str, last_updated_at: datetime) -> None: how_long_ago_was_last_update = datetime.now() - last_updated_at @@ -163,7 +220,9 @@ class LiquidationProcessor: self.state = LiquidationProcessorState.UNHEALTHY self.state_change.on_next(self) self._logger.error( - f"[{self.name}] Liquidator - last {name} update was {how_long_ago_was_last_update} ago - more than error threshold {LiquidationProcessor._AGE_ERROR_THRESHOLD}") + f"[{self.name}] Liquidator - last {name} update was {how_long_ago_was_last_update} ago - more than error threshold {LiquidationProcessor._AGE_ERROR_THRESHOLD}" + ) elif how_long_ago_was_last_update > LiquidationProcessor._AGE_WARNING_THRESHOLD: self._logger.warning( - f"[{self.name}] Liquidator - last {name} update was {how_long_ago_was_last_update} ago - more than warning threshold {LiquidationProcessor._AGE_WARNING_THRESHOLD}") + f"[{self.name}] Liquidator - last {name} update was {how_long_ago_was_last_update} ago - more than warning threshold {LiquidationProcessor._AGE_WARNING_THRESHOLD}" + ) diff --git a/mango/loadedmarket.py b/mango/loadedmarket.py index 90443a7..25a08bf 100644 --- a/mango/loadedmarket.py +++ b/mango/loadedmarket.py @@ -30,25 +30,49 @@ from .token import Instrument, Token # This class describes a crypto market. It *must* have an address, a base token and a quote token. # class LoadedMarket(Market): - def __init__(self, program_address: PublicKey, address: PublicKey, inventory_source: InventorySource, base: Instrument, quote: Token, lot_size_converter: LotSizeConverter) -> None: - super().__init__(program_address, address, inventory_source, base, quote, lot_size_converter) + def __init__( + self, + program_address: PublicKey, + address: PublicKey, + inventory_source: InventorySource, + base: Instrument, + quote: Token, + lot_size_converter: LotSizeConverter, + ) -> None: + super().__init__( + program_address, address, inventory_source, base, quote, lot_size_converter + ) @property def bids_address(self) -> PublicKey: - raise NotImplementedError("LoadedMarket.bids_address() is not implemented on the base type.") + raise NotImplementedError( + "LoadedMarket.bids_address() is not implemented on the base type." + ) @property def asks_address(self) -> PublicKey: - raise NotImplementedError("LoadedMarket.asks_address() is not implemented on the base type.") + raise NotImplementedError( + "LoadedMarket.asks_address() is not implemented on the base type." + ) - def parse_account_info_to_orders(self, account_info: AccountInfo) -> typing.Sequence[Order]: - raise NotImplementedError("LoadedMarket.parse_account_info_to_orders() is not implemented on the base type.") + def parse_account_info_to_orders( + self, account_info: AccountInfo + ) -> typing.Sequence[Order]: + raise NotImplementedError( + "LoadedMarket.parse_account_info_to_orders() is not implemented on the base type." + ) - def parse_account_infos_to_orderbook(self, bids_account_info: AccountInfo, asks_account_info: AccountInfo) -> OrderBook: + def parse_account_infos_to_orderbook( + self, bids_account_info: AccountInfo, asks_account_info: AccountInfo + ) -> OrderBook: bids_orderbook = self.parse_account_info_to_orders(bids_account_info) asks_orderbook = self.parse_account_info_to_orders(asks_account_info) - return OrderBook(self.symbol, self.lot_size_converter, bids_orderbook, asks_orderbook) + return OrderBook( + self.symbol, self.lot_size_converter, bids_orderbook, asks_orderbook + ) def fetch_orderbook(self, context: Context) -> OrderBook: - [bids_info, asks_info] = AccountInfo.load_multiple(context, [self.bids_address, self.asks_address]) + [bids_info, asks_info] = AccountInfo.load_multiple( + context, [self.bids_address, self.asks_address] + ) return self.parse_account_infos_to_orderbook(bids_info, asks_info) diff --git a/mango/logmessages.py b/mango/logmessages.py index d429aec..7cb19f5 100644 --- a/mango/logmessages.py +++ b/mango/logmessages.py @@ -18,13 +18,15 @@ import typing from .idl import IdlParser, lazy_load_cached_idl_parser -def expand_log_messages(original_messages: typing.Sequence[str]) -> typing.Sequence[str]: +def expand_log_messages( + original_messages: typing.Sequence[str], +) -> typing.Sequence[str]: idl_parser: IdlParser = lazy_load_cached_idl_parser("mango_logs.json") expanded_messages: typing.List[str] = [] parse_next_line: bool = False for message in original_messages: if parse_next_line: - encoded: str = message[len("Program log: "):] + encoded: str = message[len("Program log: ") :] name, parsed = idl_parser.decode_and_parse(encoded) expanded_messages += ["Mango " + name + " " + str(parsed)] parse_next_line = False diff --git a/mango/lotsizeconverter.py b/mango/lotsizeconverter.py index fca7155..0d30825 100644 --- a/mango/lotsizeconverter.py +++ b/mango/lotsizeconverter.py @@ -22,8 +22,14 @@ from .token import Instrument # # ๐Ÿฅญ LotSizeConverter class # -class LotSizeConverter(): - def __init__(self, base: Instrument, base_lot_size: Decimal, quote: Instrument, quote_lot_size: Decimal) -> None: +class LotSizeConverter: + def __init__( + self, + base: Instrument, + base_lot_size: Decimal, + quote: Instrument, + quote_lot_size: Decimal, + ) -> None: self.base: Instrument = base self.base_lot_size: Decimal = base_lot_size self.quote: Instrument = quote @@ -50,30 +56,33 @@ class LotSizeConverter(): return self.adjust_to_base_decimals(price_lots * lots_to_native) def price_number_to_lots(self, price: Decimal) -> int: - base_factor: Decimal = 10 ** self.base.decimals - quote_factor: Decimal = 10 ** self.quote.decimals - return round((price * quote_factor * self.base_lot_size) / (base_factor * self.quote_lot_size)) + base_factor: Decimal = 10**self.base.decimals + quote_factor: Decimal = 10**self.quote.decimals + return round( + (price * quote_factor * self.base_lot_size) + / (base_factor * self.quote_lot_size) + ) def base_size_lots_to_number(self, size_lots: Decimal) -> Decimal: size: int = round(size_lots) - base_factor: Decimal = 10 ** self.base.decimals + base_factor: Decimal = 10**self.base.decimals return Decimal(size * self.base_lot_size) / base_factor def base_size_number_to_lots(self, size: Decimal) -> int: - base_factor: Decimal = 10 ** self.base.decimals + base_factor: Decimal = 10**self.base.decimals return int(round(size * base_factor) / self.base_lot_size) def quote_size_lots_to_number(self, size_lots: Decimal) -> Decimal: size: int = round(size_lots) - quote_factor: Decimal = 10 ** self.quote.decimals + quote_factor: Decimal = 10**self.quote.decimals return Decimal(size * self.quote_lot_size) / quote_factor def quote_lots_to_number(self, size_lots: Decimal) -> Decimal: - quote_factor: Decimal = 10 ** self.quote.decimals + quote_factor: Decimal = 10**self.quote.decimals return Decimal(size_lots * self.quote_lot_size) / quote_factor def quote_size_number_to_lots(self, size: Decimal) -> int: - quote_factor: Decimal = 10 ** self.quote.decimals + quote_factor: Decimal = 10**self.quote.decimals return int(round(size * quote_factor) / self.quote_lot_size) def round_base(self, quantity: Decimal) -> Decimal: @@ -93,8 +102,12 @@ class LotSizeConverter(): # class NullLotSizeConverter(LotSizeConverter): def __init__(self) -> None: - super().__init__(Instrument("NULLBASE", "Null Base", Decimal(0)), Decimal( - 1), Instrument("NULLQUOTE", "Null Quote", Decimal(0)), Decimal(1)) + super().__init__( + Instrument("NULLBASE", "Null Base", Decimal(0)), + Decimal(1), + Instrument("NULLQUOTE", "Null Quote", Decimal(0)), + Decimal(1), + ) def price_lots_to_number(self, price_lots: Decimal) -> Decimal: return price_lots @@ -122,32 +135,42 @@ class NullLotSizeConverter(LotSizeConverter): # class RaisingLotSizeConverter(LotSizeConverter): def __init__(self) -> None: - super().__init__(Instrument("RAISINGBASE", "Raising Base", Decimal(0)), Decimal(-1), - Instrument("RAISINGQUOTE", "Raising Quote", Decimal(0)), Decimal(-1)) + super().__init__( + Instrument("RAISINGBASE", "Raising Base", Decimal(0)), + Decimal(-1), + Instrument("RAISINGQUOTE", "Raising Quote", Decimal(0)), + Decimal(-1), + ) def price_lots_to_number(self, price_lots: Decimal) -> Decimal: raise NotImplementedError( - "RaisingLotSizeConverter.price_lots_to_number() is not implemented. RaisingLotSizeConverter is a stub used where no LotSizeConverter members should be called.") + "RaisingLotSizeConverter.price_lots_to_number() is not implemented. RaisingLotSizeConverter is a stub used where no LotSizeConverter members should be called." + ) def price_number_to_lots(self, price: Decimal) -> int: raise NotImplementedError( - "RaisingLotSizeConverter.price_number_to_lots() is not implemented. RaisingLotSizeConverter is a stub used where no LotSizeConverter members should be called.") + "RaisingLotSizeConverter.price_number_to_lots() is not implemented. RaisingLotSizeConverter is a stub used where no LotSizeConverter members should be called." + ) def base_size_lots_to_number(self, size_lots: Decimal) -> Decimal: raise NotImplementedError( - "RaisingLotSizeConverter.base_size_lots_to_number() is not implemented. RaisingLotSizeConverter is a stub used where no LotSizeConverter members should be called.") + "RaisingLotSizeConverter.base_size_lots_to_number() is not implemented. RaisingLotSizeConverter is a stub used where no LotSizeConverter members should be called." + ) def base_size_number_to_lots(self, size: Decimal) -> int: raise NotImplementedError( - "RaisingLotSizeConverter.base_size_number_to_lots() is not implemented. RaisingLotSizeConverter is a stub used where no LotSizeConverter members should be called.") + "RaisingLotSizeConverter.base_size_number_to_lots() is not implemented. RaisingLotSizeConverter is a stub used where no LotSizeConverter members should be called." + ) def quote_size_lots_to_number(self, size_lots: Decimal) -> Decimal: raise NotImplementedError( - "RaisingLotSizeConverter.quote_size_lots_to_number() is not implemented. RaisingLotSizeConverter is a stub used where no LotSizeConverter members should be called.") + "RaisingLotSizeConverter.quote_size_lots_to_number() is not implemented. RaisingLotSizeConverter is a stub used where no LotSizeConverter members should be called." + ) def quote_size_number_to_lots(self, size: Decimal) -> int: raise NotImplementedError( - "RaisingLotSizeConverter.quote_size_number_to_lots() is not implemented. RaisingLotSizeConverter is a stub used where no LotSizeConverter members should be called.") + "RaisingLotSizeConverter.quote_size_number_to_lots() is not implemented. RaisingLotSizeConverter is a stub used where no LotSizeConverter members should be called." + ) def __str__(self) -> str: return "ยซ RaisingLotSizeConverter ยป" diff --git a/mango/mangoinstruction.py b/mango/mangoinstruction.py index 1992c49..2155f7c 100644 --- a/mango/mangoinstruction.py +++ b/mango/mangoinstruction.py @@ -197,7 +197,12 @@ _target_indices: typing.Dict[InstructionType, int] = { # transaction. Keeping it all together here makes many things simpler. # class MangoInstruction: - def __init__(self, instruction_type: InstructionType, instruction_data: typing.Any, accounts: typing.Sequence[PublicKey]) -> None: + def __init__( + self, + instruction_type: InstructionType, + instruction_data: typing.Any, + accounts: typing.Sequence[PublicKey], + ) -> None: self.instruction_type = instruction_type self.instruction_data = instruction_data self.accounts = accounts diff --git a/mango/market.py b/mango/market.py index 433db7c..c1b0f6c 100644 --- a/mango/market.py +++ b/mango/market.py @@ -42,7 +42,15 @@ class InventorySource(enum.Enum): # This class describes a crypto market. It *must* have an address, a base token and a quote token. # class Market(metaclass=abc.ABCMeta): - def __init__(self, program_address: PublicKey, address: PublicKey, inventory_source: InventorySource, base: Instrument, quote: Token, lot_size_converter: LotSizeConverter) -> None: + def __init__( + self, + program_address: PublicKey, + address: PublicKey, + inventory_source: InventorySource, + base: Instrument, + quote: Token, + lot_size_converter: LotSizeConverter, + ) -> None: self._logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.program_address: PublicKey = program_address self.address: PublicKey = address @@ -79,9 +87,13 @@ class DryRunMarket(Market): address: PublicKey = SYSTEM_PROGRAM_ADDRESS inventory_source: InventorySource = InventorySource.SPL_TOKENS base: Instrument = Instrument("DRYRUNBASE", "DryRunBase", Decimal(6)) - quote: Token = Token("DRYRUNQUOTE", "DryRunQuote", Decimal(6), SYSTEM_PROGRAM_ADDRESS) + quote: Token = Token( + "DRYRUNQUOTE", "DryRunQuote", Decimal(6), SYSTEM_PROGRAM_ADDRESS + ) lot_size_converter: LotSizeConverter = NullLotSizeConverter() - super().__init__(program_address, address, inventory_source, base, quote, lot_size_converter) + super().__init__( + program_address, address, inventory_source, base, quote, lot_size_converter + ) self.market_name: str = market_name @property diff --git a/mango/marketlookup.py b/mango/marketlookup.py index 97a0c50..79c09c1 100644 --- a/mango/marketlookup.py +++ b/mango/marketlookup.py @@ -28,21 +28,28 @@ from .market import Market # This base class allows specialised ways of looking up markets by symbol or by address. # + class MarketLookup(metaclass=abc.ABCMeta): def __init__(self) -> None: self._logger: logging.Logger = logging.getLogger(self.__class__.__name__) @abc.abstractmethod def find_by_symbol(self, symbol: str) -> typing.Optional[Market]: - raise NotImplementedError("MarketLookup.find_by_symbol() is not implemented on the base type.") + raise NotImplementedError( + "MarketLookup.find_by_symbol() is not implemented on the base type." + ) @abc.abstractmethod def find_by_address(self, address: PublicKey) -> typing.Optional[Market]: - raise NotImplementedError("MarketLookup.find_by_address() is not implemented on the base type.") + raise NotImplementedError( + "MarketLookup.find_by_address() is not implemented on the base type." + ) @abc.abstractmethod def all_markets(self) -> typing.Sequence[Market]: - raise NotImplementedError("MarketLookup.all_markets() is not implemented on the base type.") + raise NotImplementedError( + "MarketLookup.all_markets() is not implemented on the base type." + ) # # ๐Ÿฅญ NullMarketLookup class @@ -50,6 +57,7 @@ class MarketLookup(metaclass=abc.ABCMeta): # This class is a simple stub `MarketLookup` that never returns a `Market`. # + class NullMarketLookup(MarketLookup): def __init__(self) -> None: super().__init__() @@ -70,6 +78,7 @@ class NullMarketLookup(MarketLookup): # found. # + class CompoundMarketLookup(MarketLookup): def __init__(self, lookups: typing.Sequence[MarketLookup]) -> None: super().__init__() @@ -90,4 +99,8 @@ class CompoundMarketLookup(MarketLookup): return None def all_markets(self) -> typing.Sequence[Market]: - return [market for sublist in map(lambda lookup: lookup.all_markets(), self.lookups) for market in sublist] + return [ + market + for sublist in map(lambda lookup: lookup.all_markets(), self.lookups) + for market in sublist + ] diff --git a/mango/marketmaking/__init__.py b/mango/marketmaking/__init__.py index 038bd29..5cabcca 100644 --- a/mango/marketmaking/__init__.py +++ b/mango/marketmaking/__init__.py @@ -9,15 +9,27 @@ # from .marketmaker import MarketMaker as MarketMaker from .modelstatebuilder import ModelStateBuilder as ModelStateBuilder -from .modelstatebuilder import PerpPollingModelStateBuilder as PerpPollingModelStateBuilder +from .modelstatebuilder import ( + PerpPollingModelStateBuilder as PerpPollingModelStateBuilder, +) from .modelstatebuilder import PollingModelStateBuilder as PollingModelStateBuilder -from .modelstatebuilder import SerumPollingModelStateBuilder as SerumPollingModelStateBuilder -from .modelstatebuilder import SpotPollingModelStateBuilder as SpotPollingModelStateBuilder +from .modelstatebuilder import ( + SerumPollingModelStateBuilder as SerumPollingModelStateBuilder, +) +from .modelstatebuilder import ( + SpotPollingModelStateBuilder as SpotPollingModelStateBuilder, +) from .modelstatebuilder import WebsocketModelStateBuilder as WebsocketModelStateBuilder from .modelstatebuilderfactory import ModelUpdateMode as ModelUpdateMode -from .modelstatebuilderfactory import model_state_builder_factory as model_state_builder_factory -from .orderreconciler import AlwaysReplaceOrderReconciler as AlwaysReplaceOrderReconciler +from .modelstatebuilderfactory import ( + model_state_builder_factory as model_state_builder_factory, +) +from .orderreconciler import ( + AlwaysReplaceOrderReconciler as AlwaysReplaceOrderReconciler, +) from .orderreconciler import NullOrderReconciler as NullOrderReconciler from .orderreconciler import OrderReconciler as OrderReconciler from .reconciledorders import ReconciledOrders as ReconciledOrders -from .toleranceorderreconciler import ToleranceOrderReconciler as ToleranceOrderReconciler +from .toleranceorderreconciler import ( + ToleranceOrderReconciler as ToleranceOrderReconciler, +) diff --git a/mango/marketmaking/marketmaker.py b/mango/marketmaking/marketmaker.py index b3fa277..e0724da 100644 --- a/mango/marketmaking/marketmaker.py +++ b/mango/marketmaking/marketmaker.py @@ -32,14 +32,21 @@ from .orderchain.chain import Chain # An event-driven market-maker. # class MarketMaker: - def __init__(self, wallet: mango.Wallet, market: mango.Market, - market_instruction_builder: mango.MarketInstructionBuilder, - desired_orders_chain: Chain, order_reconciler: OrderReconciler, - redeem_threshold: typing.Optional[Decimal]) -> None: + def __init__( + self, + wallet: mango.Wallet, + market: mango.Market, + market_instruction_builder: mango.MarketInstructionBuilder, + desired_orders_chain: Chain, + order_reconciler: OrderReconciler, + redeem_threshold: typing.Optional[Decimal], + ) -> None: self._logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.wallet: mango.Wallet = wallet self.market: mango.Market = market - self.market_instruction_builder: mango.MarketInstructionBuilder = market_instruction_builder + self.market_instruction_builder: mango.MarketInstructionBuilder = ( + market_instruction_builder + ) self.desired_orders_chain: Chain = desired_orders_chain self.order_reconciler: OrderReconciler = order_reconciler self.redeem_threshold: typing.Optional[Decimal] = redeem_threshold @@ -52,7 +59,9 @@ class MarketMaker: def pulse(self, context: mango.Context, model_state: mango.ModelState) -> None: try: - self._logger.debug(f"[{context.name}] Pulse started with oracle price:\n {model_state.price}") + self._logger.debug( + f"[{context.name}] Pulse started with oracle price:\n {model_state.price}" + ) payer = mango.CombinableInstructions.from_wallet(self.wallet) @@ -65,14 +74,21 @@ class MarketMaker: # It also gives the opportunity to code outside the orderchain to set `not_quoting` if that # code has access to the `model_state`. if model_state.not_quoting: - self._logger.info(f"[{context.name}] Market-maker not quoting - model_state.not_quoting is set.") + self._logger.info( + f"[{context.name}] Market-maker not quoting - model_state.not_quoting is set." + ) return existing_orders = model_state.current_orders() - self._logger.debug(f"""Before reconciliation: all owned orders on current orderbook [{model_state.market.symbol}]: - {mango.indent_collection_as_str(existing_orders)}""") - reconciled = self.order_reconciler.reconcile(model_state, existing_orders, desired_orders) - self._logger.debug(f"""After reconciliation + self._logger.debug( + f"""Before reconciliation: all owned orders on current orderbook [{model_state.market.symbol}]: + {mango.indent_collection_as_str(existing_orders)}""" + ) + reconciled = self.order_reconciler.reconcile( + model_state, existing_orders, desired_orders + ) + self._logger.debug( + f"""After reconciliation Keep: {mango.indent_collection_as_str(reconciled.to_keep)} Cancel: @@ -80,19 +96,29 @@ Cancel: Place: {mango.indent_collection_as_str(reconciled.to_place)} Ignore: - {mango.indent_collection_as_str(reconciled.to_ignore)}""") + {mango.indent_collection_as_str(reconciled.to_ignore)}""" + ) cancellations = mango.CombinableInstructions.empty() # Perp markets have a CANCEL_ALL instruction that Spot and Serum markets don't. Use it if we can. - if reconciled.cancelling_all and isinstance(self.market_instruction_builder, mango.PerpMarketInstructionBuilder): + if reconciled.cancelling_all and isinstance( + self.market_instruction_builder, mango.PerpMarketInstructionBuilder + ): ids = [f"{ord.id} / {ord.client_id}" for ord in reconciled.to_cancel] - self._logger.info(f"Cancelling all orders on {self.market.symbol} - currently {len(ids)}: {ids}") - cancellations = self.market_instruction_builder.build_cancel_all_orders_instructions() + self._logger.info( + f"Cancelling all orders on {self.market.symbol} - currently {len(ids)}: {ids}" + ) + cancellations = ( + self.market_instruction_builder.build_cancel_all_orders_instructions() + ) else: for to_cancel in reconciled.to_cancel: self._logger.info(f"Cancelling {self.market.symbol} {to_cancel}") - cancel = self.market_instruction_builder.build_cancel_order_instructions( - to_cancel, ok_if_missing=True) + cancel = ( + self.market_instruction_builder.build_cancel_order_instructions( + to_cancel, ok_if_missing=True + ) + ) cancellations += cancel place_orders = mango.CombinableInstructions.empty() @@ -100,28 +126,51 @@ Ignore: desired_client_id: int = context.generate_client_id() to_place_with_client_id = to_place.with_client_id(desired_client_id) - self._logger.info(f"Placing {self.market.symbol} {to_place_with_client_id}") - place_order = self.market_instruction_builder.build_place_order_instructions(to_place_with_client_id) + self._logger.info( + f"Placing {self.market.symbol} {to_place_with_client_id}" + ) + place_order = ( + self.market_instruction_builder.build_place_order_instructions( + to_place_with_client_id + ) + ) place_orders += place_order - crank = self.market_instruction_builder.build_crank_instructions(model_state.accounts_to_crank) + crank = self.market_instruction_builder.build_crank_instructions( + model_state.accounts_to_crank + ) settle = self.market_instruction_builder.build_settle_instructions() redeem = mango.CombinableInstructions.empty() - if self.redeem_threshold is not None and model_state.inventory.liquidity_incentives.value > self.redeem_threshold: + if ( + self.redeem_threshold is not None + and model_state.inventory.liquidity_incentives.value + > self.redeem_threshold + ): redeem = self.market_instruction_builder.build_redeem_instructions() # Don't bother if we have no orders to change if len(cancellations.instructions) + len(place_orders.instructions) > 0: - (payer + cancellations + place_orders + crank + settle + redeem).execute(context) + ( + payer + cancellations + place_orders + crank + settle + redeem + ).execute(context) 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}] Market-maker problem on pulse: {common_exception}") + self._logger.error( + f"[{context.name}] Market-maker problem on pulse: {common_exception}" + ) self.pulse_error.on_next(common_exception) except Exception as exception: - self._logger.error(f"[{context.name}] Market-maker error on pulse:\n{traceback.format_exc()}") + self._logger.error( + f"[{context.name}] Market-maker error on pulse:\n{traceback.format_exc()}" + ) self.pulse_error.on_next(exception) def __str__(self) -> str: diff --git a/mango/marketmaking/modelstatebuilder.py b/mango/marketmaking/modelstatebuilder.py index 05c0028..2a82118 100644 --- a/mango/marketmaking/modelstatebuilder.py +++ b/mango/marketmaking/modelstatebuilder.py @@ -42,7 +42,9 @@ class ModelStateBuilder(metaclass=abc.ABCMeta): @abc.abstractmethod def build(self, context: mango.Context) -> ModelState: - raise NotImplementedError("ModelStateBuilder.build() is not implemented on the base type.") + raise NotImplementedError( + "ModelStateBuilder.build() is not implemented on the base type." + ) def __str__(self) -> str: return "ยซ ModelStateBuilder ยป" @@ -79,28 +81,62 @@ class PollingModelStateBuilder(ModelStateBuilder): started_at = time.time() built: ModelState = self.poll(context) time_taken = time.time() - started_at - self._logger.debug(f"Poll for model state complete. Time taken: {time_taken:.2f} seconds.") + self._logger.debug( + f"Poll for model state complete. Time taken: {time_taken:.2f} seconds." + ) return built @abc.abstractmethod def poll(self, context: mango.Context) -> ModelState: - raise NotImplementedError("PollingModelStateBuilder.poll() is not implemented on the base type.") + raise NotImplementedError( + "PollingModelStateBuilder.poll() is not implemented on the base type." + ) - def from_values(self, order_owner: PublicKey, market: mango.Market, group: mango.Group, account: mango.Account, - price: mango.Price, placed_orders_container: mango.PlacedOrdersContainer, - inventory: mango.Inventory, orderbook: mango.OrderBook, event_queue: mango.EventQueue) -> ModelState: - group_watcher: mango.ManualUpdateWatcher[mango.Group] = mango.ManualUpdateWatcher(group) - account_watcher: mango.ManualUpdateWatcher[mango.Account] = mango.ManualUpdateWatcher(account) - price_watcher: mango.ManualUpdateWatcher[mango.Price] = mango.ManualUpdateWatcher(price) + def from_values( + self, + order_owner: PublicKey, + market: mango.Market, + group: mango.Group, + account: mango.Account, + price: mango.Price, + placed_orders_container: mango.PlacedOrdersContainer, + inventory: mango.Inventory, + orderbook: mango.OrderBook, + event_queue: mango.EventQueue, + ) -> ModelState: + group_watcher: mango.ManualUpdateWatcher[ + mango.Group + ] = mango.ManualUpdateWatcher(group) + account_watcher: mango.ManualUpdateWatcher[ + mango.Account + ] = mango.ManualUpdateWatcher(account) + price_watcher: mango.ManualUpdateWatcher[ + mango.Price + ] = mango.ManualUpdateWatcher(price) placed_orders_container_watcher: mango.ManualUpdateWatcher[ - mango.PlacedOrdersContainer] = mango.ManualUpdateWatcher(placed_orders_container) - inventory_watcher: mango.ManualUpdateWatcher[mango.Inventory] = mango.ManualUpdateWatcher(inventory) - orderbook_watcher: mango.ManualUpdateWatcher[mango.OrderBook] = mango.ManualUpdateWatcher(orderbook) - event_queue_watcher: mango.ManualUpdateWatcher[mango.EventQueue] = mango.ManualUpdateWatcher(event_queue) + mango.PlacedOrdersContainer + ] = mango.ManualUpdateWatcher(placed_orders_container) + inventory_watcher: mango.ManualUpdateWatcher[ + mango.Inventory + ] = mango.ManualUpdateWatcher(inventory) + orderbook_watcher: mango.ManualUpdateWatcher[ + mango.OrderBook + ] = mango.ManualUpdateWatcher(orderbook) + event_queue_watcher: mango.ManualUpdateWatcher[ + mango.EventQueue + ] = mango.ManualUpdateWatcher(event_queue) - return ModelState(order_owner, market, group_watcher, account_watcher, price_watcher, - placed_orders_container_watcher, inventory_watcher, orderbook_watcher, - event_queue_watcher) + return ModelState( + order_owner, + market, + group_watcher, + account_watcher, + price_watcher, + placed_orders_container_watcher, + inventory_watcher, + orderbook_watcher, + event_queue_watcher, + ) def __str__(self) -> str: return "ยซ PollingModelStateBuilder ยป" @@ -111,17 +147,18 @@ class PollingModelStateBuilder(ModelStateBuilder): # Polls Solana and builds a `ModelState` for a `SerumMarket` # class SerumPollingModelStateBuilder(PollingModelStateBuilder): - def __init__(self, - order_owner: PublicKey, - market: mango.SerumMarket, - oracle: mango.Oracle, - group_address: PublicKey, - cache_address: PublicKey, - account_address: PublicKey, - open_orders_address: PublicKey, - base_inventory_token_account: mango.TokenAccount, - quote_inventory_token_account: mango.TokenAccount, - ) -> None: + def __init__( + self, + order_owner: PublicKey, + market: mango.SerumMarket, + oracle: mango.Oracle, + group_address: PublicKey, + cache_address: PublicKey, + account_address: PublicKey, + open_orders_address: PublicKey, + base_inventory_token_account: mango.TokenAccount, + quote_inventory_token_account: mango.TokenAccount, + ) -> None: super().__init__() self.order_owner: PublicKey = order_owner self.market: mango.SerumMarket = market @@ -131,12 +168,20 @@ class SerumPollingModelStateBuilder(PollingModelStateBuilder): self.cache_address: PublicKey = cache_address self.account_address: PublicKey = account_address self.open_orders_address: PublicKey = open_orders_address - self.base_inventory_token_account: mango.TokenAccount = base_inventory_token_account - self.quote_inventory_token_account: mango.TokenAccount = quote_inventory_token_account + self.base_inventory_token_account: mango.TokenAccount = ( + base_inventory_token_account + ) + self.quote_inventory_token_account: mango.TokenAccount = ( + quote_inventory_token_account + ) # Serum always uses Tokens - self.base_token: Token = Token.ensure(self.base_inventory_token_account.value.token) - self.quote_token: Token = Token.ensure(self.quote_inventory_token_account.value.token) + self.base_token: Token = Token.ensure( + self.base_inventory_token_account.value.token + ) + self.quote_token: Token = Token.ensure( + self.quote_inventory_token_account.value.token + ) def poll(self, context: mango.Context) -> ModelState: addresses: typing.List[PublicKey] = [ @@ -148,40 +193,68 @@ class SerumPollingModelStateBuilder(PollingModelStateBuilder): self.quote_inventory_token_account.address, self.market.bids_address, self.market.asks_address, - self.market.event_queue_address + self.market.event_queue_address, ] - account_infos: typing.Sequence[mango.AccountInfo] = mango.AccountInfo.load_multiple(context, addresses) + account_infos: typing.Sequence[ + mango.AccountInfo + ] = mango.AccountInfo.load_multiple(context, addresses) group: mango.Group = mango.Group.parse_with_context(context, account_infos[0]) cache: mango.Cache = mango.Cache.parse(account_infos[1]) account: mango.Account = mango.Account.parse(account_infos[2], group, cache) placed_orders_container: mango.PlacedOrdersContainer = mango.OpenOrders.parse( - account_infos[3], self.market.base.decimals, self.market.quote.decimals) + account_infos[3], self.market.base.decimals, self.market.quote.decimals + ) # Serum markets don't accrue MNGO liquidity incentives - mngo_accrued: InstrumentValue = InstrumentValue(group.liquidity_incentive_token, Decimal(0)) + mngo_accrued: InstrumentValue = InstrumentValue( + group.liquidity_incentive_token, Decimal(0) + ) - base_inventory_token_account = mango.TokenAccount.parse(account_infos[4], self.base_token) - quote_inventory_token_account = mango.TokenAccount.parse(account_infos[5], self.quote_token) + base_inventory_token_account = mango.TokenAccount.parse( + account_infos[4], self.base_token + ) + quote_inventory_token_account = mango.TokenAccount.parse( + account_infos[5], self.quote_token + ) - orderbook: mango.OrderBook = self.market.parse_account_infos_to_orderbook(account_infos[6], account_infos[7]) + orderbook: mango.OrderBook = self.market.parse_account_infos_to_orderbook( + account_infos[6], account_infos[7] + ) event_queue: mango.EventQueue = mango.SerumEventQueue.parse(account_infos[8]) price: mango.Price = self.oracle.fetch_price(context) - available: Decimal = (base_inventory_token_account.value.value * price.mid_price) + \ - quote_inventory_token_account.value.value - available_collateral: InstrumentValue = InstrumentValue(quote_inventory_token_account.value.token, available) - inventory: mango.Inventory = mango.Inventory(mango.InventorySource.SPL_TOKENS, - mngo_accrued, - available_collateral, - base_inventory_token_account.value, - quote_inventory_token_account.value) + available: Decimal = ( + base_inventory_token_account.value.value * price.mid_price + ) + quote_inventory_token_account.value.value + available_collateral: InstrumentValue = InstrumentValue( + quote_inventory_token_account.value.token, available + ) + inventory: mango.Inventory = mango.Inventory( + mango.InventorySource.SPL_TOKENS, + mngo_accrued, + available_collateral, + base_inventory_token_account.value, + quote_inventory_token_account.value, + ) - return self.from_values(self.order_owner, self.market, group, account, price, placed_orders_container, inventory, orderbook, event_queue) + return self.from_values( + self.order_owner, + self.market, + group, + account, + price, + placed_orders_container, + inventory, + orderbook, + event_queue, + ) def __str__(self) -> str: - return f"""ยซ SerumPollingModelStateBuilder for market '{self.market.symbol}' ยป""" + return ( + f"""ยซ SerumPollingModelStateBuilder for market '{self.market.symbol}' ยป""" + ) # # ๐Ÿฅญ SpotPollingModelStateBuilder class @@ -189,16 +262,17 @@ class SerumPollingModelStateBuilder(PollingModelStateBuilder): # Polls Solana and builds a `ModelState` for a `SpotMarket` # class SpotPollingModelStateBuilder(PollingModelStateBuilder): - def __init__(self, - order_owner: PublicKey, - market: mango.SpotMarket, - oracle: mango.Oracle, - group_address: PublicKey, - cache_address: PublicKey, - account_address: PublicKey, - open_orders_address: PublicKey, - all_open_orders_addresses: typing.Sequence[PublicKey] - ) -> None: + def __init__( + self, + order_owner: PublicKey, + market: mango.SpotMarket, + oracle: mango.Oracle, + group_address: PublicKey, + cache_address: PublicKey, + account_address: PublicKey, + open_orders_address: PublicKey, + all_open_orders_addresses: typing.Sequence[PublicKey], + ) -> None: super().__init__() self.order_owner: PublicKey = order_owner self.market: mango.SpotMarket = market @@ -208,7 +282,9 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder): self.cache_address: PublicKey = cache_address self.account_address: PublicKey = account_address self.open_orders_address: PublicKey = open_orders_address - self.all_open_orders_addresses: typing.Sequence[PublicKey] = all_open_orders_addresses + self.all_open_orders_addresses: typing.Sequence[ + PublicKey + ] = all_open_orders_addresses self.collateral_calculator: CollateralCalculator = SpotCollateralCalculator() @@ -220,9 +296,11 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder): self.market.bids_address, self.market.asks_address, self.market.event_queue_address, - *self.all_open_orders_addresses + *self.all_open_orders_addresses, ] - account_infos: typing.Sequence[mango.AccountInfo] = mango.AccountInfo.load_multiple(context, addresses) + account_infos: typing.Sequence[ + mango.AccountInfo + ] = mango.AccountInfo.load_multiple(context, addresses) group: mango.Group = mango.Group.parse_with_context(context, account_infos[0]) cache: mango.Cache = mango.Cache.parse(account_infos[1]) account: mango.Account = mango.Account.parse(account_infos[2], group, cache) @@ -231,42 +309,75 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder): self.all_open_orders_addresses = account.spot_open_orders spot_open_orders_account_infos_by_address = { - str(account_info.address): account_info for account_info in account_infos[6:]} + str(account_info.address): account_info + for account_info in account_infos[6:] + } all_open_orders: typing.Dict[str, mango.OpenOrders] = {} for basket_token in account.slots: - if basket_token.spot_open_orders is not None and str(basket_token.spot_open_orders) in spot_open_orders_account_infos_by_address: - account_info: mango.AccountInfo = spot_open_orders_account_infos_by_address[str( - basket_token.spot_open_orders)] + if ( + basket_token.spot_open_orders is not None + and str(basket_token.spot_open_orders) + in spot_open_orders_account_infos_by_address + ): + account_info: mango.AccountInfo = ( + spot_open_orders_account_infos_by_address[ + str(basket_token.spot_open_orders) + ] + ) open_orders: mango.OpenOrders = mango.OpenOrders.parse( account_info, basket_token.base_instrument.decimals, - account.shared_quote_token.decimals) + account.shared_quote_token.decimals, + ) all_open_orders[str(basket_token.spot_open_orders)] = open_orders - placed_orders_container: mango.PlacedOrdersContainer = all_open_orders[str(self.open_orders_address)] + placed_orders_container: mango.PlacedOrdersContainer = all_open_orders[ + str(self.open_orders_address) + ] # Spot markets don't accrue MNGO liquidity incentives - mngo_accrued: InstrumentValue = InstrumentValue(group.liquidity_incentive_token, Decimal(0)) + mngo_accrued: InstrumentValue = InstrumentValue( + group.liquidity_incentive_token, Decimal(0) + ) - base_value = mango.InstrumentValue.find_by_symbol(account.net_values, self.market.base.symbol) - quote_value = mango.InstrumentValue.find_by_symbol(account.net_values, self.market.quote.symbol) + base_value = mango.InstrumentValue.find_by_symbol( + account.net_values, self.market.base.symbol + ) + quote_value = mango.InstrumentValue.find_by_symbol( + account.net_values, self.market.quote.symbol + ) available_collateral: InstrumentValue = self.collateral_calculator.calculate( - account, all_open_orders, group, cache) - inventory: mango.Inventory = mango.Inventory(mango.InventorySource.ACCOUNT, - mngo_accrued, - available_collateral, - base_value, - quote_value) + account, all_open_orders, group, cache + ) + inventory: mango.Inventory = mango.Inventory( + mango.InventorySource.ACCOUNT, + mngo_accrued, + available_collateral, + base_value, + quote_value, + ) - orderbook: mango.OrderBook = self.market.parse_account_infos_to_orderbook(account_infos[3], account_infos[4]) + orderbook: mango.OrderBook = self.market.parse_account_infos_to_orderbook( + account_infos[3], account_infos[4] + ) event_queue: mango.EventQueue = mango.SerumEventQueue.parse(account_infos[5]) price: mango.Price = self.oracle.fetch_price(context) - return self.from_values(self.order_owner, self.market, group, account, price, placed_orders_container, inventory, orderbook, event_queue) + return self.from_values( + self.order_owner, + self.market, + group, + account, + price, + placed_orders_container, + inventory, + orderbook, + event_queue, + ) def __str__(self) -> str: return f"""ยซ SpotPollingModelStateBuilder for market '{self.market.symbol}' ยป""" @@ -277,13 +388,14 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder): # Polls Solana and builds a `ModelState` for a `PerpMarket` # class PerpPollingModelStateBuilder(PollingModelStateBuilder): - def __init__(self, - order_owner: PublicKey, - market: mango.PerpMarket, - oracle: mango.Oracle, - group_address: PublicKey, - cache_address: PublicKey - ) -> None: + def __init__( + self, + order_owner: PublicKey, + market: mango.PerpMarket, + oracle: mango.Oracle, + group_address: PublicKey, + cache_address: PublicKey, + ) -> None: super().__init__() self.order_owner: PublicKey = order_owner self.market: mango.PerpMarket = market @@ -301,9 +413,11 @@ class PerpPollingModelStateBuilder(PollingModelStateBuilder): self.order_owner, self.market.underlying_perp_market.bids, self.market.underlying_perp_market.asks, - self.market.event_queue_address + self.market.event_queue_address, ] - account_infos: typing.Sequence[mango.AccountInfo] = mango.AccountInfo.load_multiple(context, addresses) + account_infos: typing.Sequence[ + mango.AccountInfo + ] = mango.AccountInfo.load_multiple(context, addresses) group: mango.Group = mango.Group.parse_with_context(context, account_infos[0]) cache: mango.Cache = mango.Cache.parse(account_infos[1]) account: mango.Account = mango.Account.parse(account_infos[2], group, cache) @@ -311,27 +425,47 @@ class PerpPollingModelStateBuilder(PollingModelStateBuilder): slot = group.slot_by_perp_market_address(self.market.address) perp_account = account.perp_accounts_by_index[slot.index] if perp_account is None: - raise Exception(f"Could not find perp account at index {slot.index} of account {account.address}.") + raise Exception( + f"Could not find perp account at index {slot.index} of account {account.address}." + ) placed_orders_container: mango.PlacedOrdersContainer = perp_account.open_orders base_lots = perp_account.base_position base_value = self.market.lot_size_converter.base_size_lots_to_number(base_lots) base_token_value = mango.InstrumentValue(self.market.base, base_value) quote_token_value = account.shared_quote.net_value - available_collateral: InstrumentValue = self.collateral_calculator.calculate(account, {}, group, cache) - inventory: mango.Inventory = mango.Inventory(mango.InventorySource.ACCOUNT, - perp_account.mngo_accrued, - available_collateral, - base_token_value, - quote_token_value) + available_collateral: InstrumentValue = self.collateral_calculator.calculate( + account, {}, group, cache + ) + inventory: mango.Inventory = mango.Inventory( + mango.InventorySource.ACCOUNT, + perp_account.mngo_accrued, + available_collateral, + base_token_value, + quote_token_value, + ) - orderbook: mango.OrderBook = self.market.parse_account_infos_to_orderbook(account_infos[3], account_infos[4]) + orderbook: mango.OrderBook = self.market.parse_account_infos_to_orderbook( + account_infos[3], account_infos[4] + ) - event_queue: mango.EventQueue = mango.PerpEventQueue.parse(account_infos[5], self.market.lot_size_converter) + event_queue: mango.EventQueue = mango.PerpEventQueue.parse( + account_infos[5], self.market.lot_size_converter + ) price: mango.Price = self.oracle.fetch_price(context) - return self.from_values(self.order_owner, self.market, group, account, price, placed_orders_container, inventory, orderbook, event_queue) + return self.from_values( + self.order_owner, + self.market, + group, + account, + price, + placed_orders_container, + inventory, + orderbook, + event_queue, + ) def __str__(self) -> str: return f"""ยซ PerpPollingModelStateBuilder for market '{self.market.symbol}' ยป""" diff --git a/mango/marketmaking/modelstatebuilderfactory.py b/mango/marketmaking/modelstatebuilderfactory.py index 2259bdd..8e119e4 100644 --- a/mango/marketmaking/modelstatebuilderfactory.py +++ b/mango/marketmaking/modelstatebuilderfactory.py @@ -21,7 +21,13 @@ from solana.publickey import PublicKey from ..constants import SYSTEM_PROGRAM_ADDRESS from ..modelstate import ModelState -from .modelstatebuilder import ModelStateBuilder, WebsocketModelStateBuilder, SerumPollingModelStateBuilder, SpotPollingModelStateBuilder, PerpPollingModelStateBuilder +from .modelstatebuilder import ( + ModelStateBuilder, + WebsocketModelStateBuilder, + SerumPollingModelStateBuilder, + SpotPollingModelStateBuilder, + PerpPollingModelStateBuilder, +) class ModelUpdateMode(enum.Enum): @@ -40,21 +46,48 @@ class ModelUpdateMode(enum.Enum): # # Base class for building a `ModelState` through polling or websockets. # -def model_state_builder_factory(mode: ModelUpdateMode, context: mango.Context, disposer: mango.DisposePropagator, - websocket_manager: mango.WebSocketSubscriptionManager, health_check: mango.HealthCheck, - wallet: mango.Wallet, group: mango.Group, account: mango.Account, - market: mango.Market, oracle: mango.Oracle) -> ModelStateBuilder: +def model_state_builder_factory( + mode: ModelUpdateMode, + context: mango.Context, + disposer: mango.DisposePropagator, + websocket_manager: mango.WebSocketSubscriptionManager, + health_check: mango.HealthCheck, + wallet: mango.Wallet, + group: mango.Group, + account: mango.Account, + market: mango.Market, + oracle: mango.Oracle, +) -> ModelStateBuilder: if mode == ModelUpdateMode.WEBSOCKET: - return _websocket_model_state_builder_factory(context, disposer, websocket_manager, health_check, wallet, group, account, market, oracle) + return _websocket_model_state_builder_factory( + context, + disposer, + websocket_manager, + health_check, + wallet, + group, + account, + market, + oracle, + ) else: - return _polling_model_state_builder_factory(context, wallet, group, account, market, oracle) + return _polling_model_state_builder_factory( + context, wallet, group, account, market, oracle + ) -def _polling_model_state_builder_factory(context: mango.Context, wallet: mango.Wallet, group: mango.Group, - account: mango.Account, market: mango.Market, - oracle: mango.Oracle) -> ModelStateBuilder: +def _polling_model_state_builder_factory( + context: mango.Context, + wallet: mango.Wallet, + group: mango.Group, + account: mango.Account, + market: mango.Market, + oracle: mango.Oracle, +) -> ModelStateBuilder: if isinstance(market, mango.SerumMarket): - return _polling_serum_model_state_builder_factory(context, wallet, group, account, market, oracle) + return _polling_serum_model_state_builder_factory( + context, wallet, group, account, market, oracle + ) elif isinstance(market, mango.SpotMarket): return _polling_spot_model_state_builder_factory(group, account, market, oracle) elif isinstance(market, mango.PerpMarket): @@ -63,55 +96,112 @@ def _polling_model_state_builder_factory(context: mango.Context, wallet: mango.W raise Exception(f"Could not determine type of market {market.symbol}") -def _polling_serum_model_state_builder_factory(context: mango.Context, wallet: mango.Wallet, group: mango.Group, - account: mango.Account, market: mango.SerumMarket, - oracle: mango.Oracle) -> ModelStateBuilder: +def _polling_serum_model_state_builder_factory( + context: mango.Context, + wallet: mango.Wallet, + group: mango.Group, + account: mango.Account, + market: mango.SerumMarket, + oracle: mango.Oracle, +) -> ModelStateBuilder: base_account = mango.TokenAccount.fetch_largest_for_owner_and_token( - context, wallet.address, market.base) + context, wallet.address, market.base + ) if base_account is None: raise Exception( - f"Could not find token account owned by {wallet.address} for base token {market.base}.") + f"Could not find token account owned by {wallet.address} for base token {market.base}." + ) quote_account = mango.TokenAccount.fetch_largest_for_owner_and_token( - context, wallet.address, market.quote) + context, wallet.address, market.quote + ) if quote_account is None: raise Exception( - f"Could not find token account owned by {wallet.address} for quote token {market.quote}.") + f"Could not find token account owned by {wallet.address} for quote token {market.quote}." + ) all_open_orders = mango.OpenOrders.load_for_market_and_owner( - context, market.address, wallet.address, context.serum_program_address, market.base.decimals, market.quote.decimals) + context, + market.address, + wallet.address, + context.serum_program_address, + market.base.decimals, + market.quote.decimals, + ) if len(all_open_orders) == 0: raise Exception( - f"Could not find serum openorders account owned by {wallet.address} for market {market.symbol}.") + f"Could not find serum openorders account owned by {wallet.address} for market {market.symbol}." + ) return SerumPollingModelStateBuilder( - all_open_orders[0].address, market, oracle, group.address, group.cache, account.address, all_open_orders[0].address, base_account, quote_account) + all_open_orders[0].address, + market, + oracle, + group.address, + group.cache, + account.address, + all_open_orders[0].address, + base_account, + quote_account, + ) -def _polling_spot_model_state_builder_factory(group: mango.Group, account: mango.Account, market: mango.SpotMarket, - oracle: mango.Oracle) -> ModelStateBuilder: +def _polling_spot_model_state_builder_factory( + group: mango.Group, + account: mango.Account, + market: mango.SpotMarket, + oracle: mango.Oracle, +) -> ModelStateBuilder: market_index: int = group.slot_by_spot_market_address(market.address).index - open_orders_address: typing.Optional[PublicKey] = account.spot_open_orders_by_index[market_index] + open_orders_address: typing.Optional[PublicKey] = account.spot_open_orders_by_index[ + market_index + ] all_open_orders_addresses: typing.Sequence[PublicKey] = account.spot_open_orders if open_orders_address is None: raise Exception( - f"Could not find spot openorders in account {account.address} for market {market.symbol}.") + f"Could not find spot openorders in account {account.address} for market {market.symbol}." + ) return SpotPollingModelStateBuilder( - open_orders_address, market, oracle, group.address, group.cache, account.address, open_orders_address, all_open_orders_addresses) + open_orders_address, + market, + oracle, + group.address, + group.cache, + account.address, + open_orders_address, + all_open_orders_addresses, + ) -def _polling_perp_model_state_builder_factory(group: mango.Group, account: mango.Account, market: mango.PerpMarket, - oracle: mango.Oracle) -> ModelStateBuilder: - return PerpPollingModelStateBuilder(account.address, market, oracle, group.address, group.cache) +def _polling_perp_model_state_builder_factory( + group: mango.Group, + account: mango.Account, + market: mango.PerpMarket, + oracle: mango.Oracle, +) -> ModelStateBuilder: + return PerpPollingModelStateBuilder( + account.address, market, oracle, group.address, group.cache + ) -def _websocket_model_state_builder_factory(context: mango.Context, disposer: mango.DisposePropagator, - websocket_manager: mango.WebSocketSubscriptionManager, - health_check: mango.HealthCheck, wallet: mango.Wallet, - group: mango.Group, account: mango.Account, market: mango.Market, - oracle: mango.Oracle) -> ModelStateBuilder: - group_watcher = mango.build_group_watcher(context, websocket_manager, health_check, group) +def _websocket_model_state_builder_factory( + context: mango.Context, + disposer: mango.DisposePropagator, + websocket_manager: mango.WebSocketSubscriptionManager, + health_check: mango.HealthCheck, + wallet: mango.Wallet, + group: mango.Group, + account: mango.Account, + market: mango.Market, + oracle: mango.Oracle, +) -> ModelStateBuilder: + group_watcher = mango.build_group_watcher( + context, websocket_manager, health_check, group + ) cache = mango.Cache.load(context, group.cache) - cache_watcher = mango.build_cache_watcher(context, websocket_manager, health_check, cache, group) + cache_watcher = mango.build_cache_watcher( + context, websocket_manager, health_check, cache, group + ) account_subscription, latest_account_observer = mango.build_account_watcher( - context, websocket_manager, health_check, account, group_watcher, cache_watcher) + context, websocket_manager, health_check, account, group_watcher, cache_watcher + ) initial_price = oracle.fetch_price(context) price_feed = oracle.to_streaming_observable(context) @@ -122,21 +212,44 @@ def _websocket_model_state_builder_factory(context: mango.Context, disposer: man market = mango.ensure_market_loaded(context, market) if isinstance(market, mango.SerumMarket): - order_owner: PublicKey = market.find_openorders_address_for_owner( - context, wallet.address) or SYSTEM_PROGRAM_ADDRESS + order_owner: PublicKey = ( + market.find_openorders_address_for_owner(context, wallet.address) + or SYSTEM_PROGRAM_ADDRESS + ) price_watcher: mango.Watcher[mango.Price] = mango.build_price_watcher( - context, websocket_manager, health_check, disposer, "market", market) - inventory_watcher: mango.Watcher[mango.Inventory] = mango.build_serum_inventory_watcher( - context, websocket_manager, health_check, disposer, wallet, market, price_watcher) - latest_open_orders_observer: mango.Watcher[mango.PlacedOrdersContainer] = mango.build_serum_open_orders_watcher( - context, websocket_manager, health_check, market, wallet) - latest_orderbook_watcher: mango.Watcher[mango.OrderBook] = mango.build_orderbook_watcher( - context, websocket_manager, health_check, market) - latest_event_queue_watcher: mango.Watcher[mango.EventQueue] = mango.build_serum_event_queue_watcher( - context, websocket_manager, health_check, market) + context, websocket_manager, health_check, disposer, "market", market + ) + inventory_watcher: mango.Watcher[ + mango.Inventory + ] = mango.build_serum_inventory_watcher( + context, + websocket_manager, + health_check, + disposer, + wallet, + market, + price_watcher, + ) + latest_open_orders_observer: mango.Watcher[ + mango.PlacedOrdersContainer + ] = mango.build_serum_open_orders_watcher( + context, websocket_manager, health_check, market, wallet + ) + latest_orderbook_watcher: mango.Watcher[ + mango.OrderBook + ] = mango.build_orderbook_watcher( + context, websocket_manager, health_check, market + ) + latest_event_queue_watcher: mango.Watcher[ + mango.EventQueue + ] = mango.build_serum_event_queue_watcher( + context, websocket_manager, health_check, market + ) elif isinstance(market, mango.SpotMarket): market_index: int = group.slot_by_spot_market_address(market.address).index - order_owner = account.spot_open_orders_by_index[market_index] or SYSTEM_PROGRAM_ADDRESS + order_owner = ( + account.spot_open_orders_by_index[market_index] or SYSTEM_PROGRAM_ADDRESS + ) all_open_orders_watchers: typing.List[mango.Watcher[mango.OpenOrders]] = [] for basket_token in account.base_slots: @@ -148,31 +261,66 @@ def _websocket_model_state_builder_factory(context: mango.Context, disposer: man if not isinstance(spot_market, mango.SpotMarket): raise Exception(f"Market {spot_market_symbol} is not a spot market") oo_watcher = mango.build_spot_open_orders_watcher( - context, websocket_manager, health_check, wallet, account, group, spot_market) + context, + websocket_manager, + health_check, + wallet, + account, + group, + spot_market, + ) all_open_orders_watchers += [oo_watcher] - if market.base == spot_market.base and market.quote == spot_market.quote: + if ( + market.base == spot_market.base + and market.quote == spot_market.quote + ): latest_open_orders_observer = oo_watcher inventory_watcher = mango.SpotInventoryAccountWatcher( - market, latest_account_observer, group_watcher, all_open_orders_watchers, cache_watcher) + market, + latest_account_observer, + group_watcher, + all_open_orders_watchers, + cache_watcher, + ) latest_orderbook_watcher = mango.build_orderbook_watcher( - context, websocket_manager, health_check, market) + context, websocket_manager, health_check, market + ) latest_event_queue_watcher = mango.build_spot_event_queue_watcher( - context, websocket_manager, health_check, market) + context, websocket_manager, health_check, market + ) elif isinstance(market, mango.PerpMarket): order_owner = account.address inventory_watcher = mango.PerpInventoryAccountWatcher( - market, latest_account_observer, group_watcher, cache_watcher, group) + market, latest_account_observer, group_watcher, cache_watcher, group + ) latest_open_orders_observer = mango.build_perp_open_orders_watcher( - context, websocket_manager, health_check, market, account, group, account_subscription) + context, + websocket_manager, + health_check, + market, + account, + group, + account_subscription, + ) latest_orderbook_watcher = mango.build_orderbook_watcher( - context, websocket_manager, health_check, market) + context, websocket_manager, health_check, market + ) latest_event_queue_watcher = mango.build_perp_event_queue_watcher( - context, websocket_manager, health_check, market) + context, websocket_manager, health_check, market + ) else: raise Exception(f"Could not determine type of market {market.symbol}") - model_state = ModelState(order_owner, market, group_watcher, latest_account_observer, - latest_price_observer, latest_open_orders_observer, - inventory_watcher, latest_orderbook_watcher, latest_event_queue_watcher) + model_state = ModelState( + order_owner, + market, + group_watcher, + latest_account_observer, + latest_price_observer, + latest_open_orders_observer, + inventory_watcher, + latest_orderbook_watcher, + latest_event_queue_watcher, + ) return WebsocketModelStateBuilder(model_state) diff --git a/mango/marketmaking/orderchain/afteraccumulateddepthelement.py b/mango/marketmaking/orderchain/afteraccumulateddepthelement.py index cf67544..b4f67fc 100644 --- a/mango/marketmaking/orderchain/afteraccumulateddepthelement.py +++ b/mango/marketmaking/orderchain/afteraccumulateddepthelement.py @@ -35,23 +35,39 @@ from ...modelstate import ModelState # its price and the mid-price. # class AfterAccumulatedDepthElement(Element): - def __init__(self, depth: typing.Optional[Decimal], adjustment_ticks: Decimal = Decimal(1)) -> None: + def __init__( + self, depth: typing.Optional[Decimal], adjustment_ticks: Decimal = Decimal(1) + ) -> None: super().__init__() self.depth: typing.Optional[Decimal] = depth self.adjustment_ticks: Decimal = adjustment_ticks @staticmethod def add_command_line_parameters(parser: argparse.ArgumentParser) -> None: - parser.add_argument("--afteraccumulateddepth-depth", type=Decimal, - help="optional fixed depth used to determine where in orderbook to place order. If not specified, the order quantity is used.") - parser.add_argument("--afteraccumulateddepth-adjustment-ticks", type=Decimal, default=Decimal(1), - help="number of ticks above/below the accumulated depth to place the order. Default is 1 tick above for a SELL, 1 tick below for a BUY. Use 0 to specify placing the order AT the depth.") + parser.add_argument( + "--afteraccumulateddepth-depth", + type=Decimal, + help="optional fixed depth used to determine where in orderbook to place order. If not specified, the order quantity is used.", + ) + parser.add_argument( + "--afteraccumulateddepth-adjustment-ticks", + type=Decimal, + default=Decimal(1), + help="number of ticks above/below the accumulated depth to place the order. Default is 1 tick above for a SELL, 1 tick below for a BUY. Use 0 to specify placing the order AT the depth.", + ) @staticmethod - def from_command_line_parameters(args: argparse.Namespace) -> "AfterAccumulatedDepthElement": - return AfterAccumulatedDepthElement(args.afteraccumulateddepth_depth, args.afteraccumulateddepth_adjustment_ticks) + def from_command_line_parameters( + args: argparse.Namespace, + ) -> "AfterAccumulatedDepthElement": + return AfterAccumulatedDepthElement( + args.afteraccumulateddepth_depth, + args.afteraccumulateddepth_adjustment_ticks, + ) - def _accumulated_quantity_exceeds_order(self, orders: typing.Sequence[mango.Order], owner: PublicKey, quantity: Decimal) -> typing.Optional[mango.Order]: + def _accumulated_quantity_exceeds_order( + self, orders: typing.Sequence[mango.Order], owner: PublicKey, quantity: Decimal + ) -> typing.Optional[mango.Order]: accumulated_quantity: Decimal = Decimal(0) for order in orders: if order.owner != owner: @@ -61,32 +77,49 @@ class AfterAccumulatedDepthElement(Element): return order return None - def process(self, context: mango.Context, model_state: ModelState, orders: typing.Sequence[mango.Order]) -> typing.Sequence[mango.Order]: + def process( + self, + context: mango.Context, + model_state: ModelState, + orders: typing.Sequence[mango.Order], + ) -> typing.Sequence[mango.Order]: new_orders: typing.List[mango.Order] = [] - adjustment: Decimal = self.adjustment_ticks * model_state.market.lot_size_converter.tick_size + adjustment: Decimal = ( + self.adjustment_ticks * model_state.market.lot_size_converter.tick_size + ) for order in orders: new_price: typing.Optional[Decimal] = None depth: Decimal = self.depth or order.quantity if order.side == mango.Side.BUY: - place_below: typing.Optional[mango.Order] = self._accumulated_quantity_exceeds_order( - model_state.bids, model_state.order_owner, depth) + place_below: typing.Optional[ + mango.Order + ] = self._accumulated_quantity_exceeds_order( + model_state.bids, model_state.order_owner, depth + ) if place_below is not None: new_price = place_below.price - adjustment else: - place_above: typing.Optional[mango.Order] = self._accumulated_quantity_exceeds_order( - model_state.asks, model_state.order_owner, depth) + place_above: typing.Optional[ + mango.Order + ] = self._accumulated_quantity_exceeds_order( + model_state.asks, model_state.order_owner, depth + ) if place_above is not None: new_price = place_above.price + adjustment if new_price is None: - self._logger.debug(f"""Order change - no acceptable depth for quantity {depth} so removing: + self._logger.debug( + f"""Order change - no acceptable depth for quantity {depth} so removing: Old: {order} - New: None""") + New: None""" + ) else: new_order: mango.Order = order.with_price(new_price) - self._logger.debug(f"""Order change - accumulated depth of {depth} is {self.adjustment_ticks} tick from {new_price}: + self._logger.debug( + f"""Order change - accumulated depth of {depth} is {self.adjustment_ticks} tick from {new_price}: Old: {order} - New: {new_order}""") + New: {new_order}""" + ) new_orders += [new_order] return new_orders diff --git a/mango/marketmaking/orderchain/biasquantityonpositionelement.py b/mango/marketmaking/orderchain/biasquantityonpositionelement.py index 7eb7ca2..9604395 100644 --- a/mango/marketmaking/orderchain/biasquantityonpositionelement.py +++ b/mango/marketmaking/orderchain/biasquantityonpositionelement.py @@ -56,56 +56,99 @@ from ...modelstate import ModelState # docs/BiasQuantityOnPosition.ods spreadsheet. # class BiasQuantityOnPositionElement(PairwiseElement): - def __init__(self, maximum_position: Decimal, target_position: Decimal = Decimal(0)) -> None: + def __init__( + self, maximum_position: Decimal, target_position: Decimal = Decimal(0) + ) -> None: super().__init__() self.maximum_position: Decimal = maximum_position self.target_position: Decimal = target_position @staticmethod def add_command_line_parameters(parser: argparse.ArgumentParser) -> None: - parser.add_argument("--biasquantityonposition-maximum-position", type=Decimal, - help="maximum inventory position to proportionally move away from") - parser.add_argument("--biasquantityonposition-target-position", type=Decimal, - help="inventory position to target (default 0)") + parser.add_argument( + "--biasquantityonposition-maximum-position", + type=Decimal, + help="maximum inventory position to proportionally move away from", + ) + parser.add_argument( + "--biasquantityonposition-target-position", + type=Decimal, + help="inventory position to target (default 0)", + ) @staticmethod - def from_command_line_parameters(args: argparse.Namespace) -> "BiasQuantityOnPositionElement": + def from_command_line_parameters( + args: argparse.Namespace, + ) -> "BiasQuantityOnPositionElement": if args.biasquantityonposition_maximum_position is None: raise Exception( - "No maximum position value specified for biasing. Try the --biasquantityonposition-maximum-position parameter?") + "No maximum position value specified for biasing. Try the --biasquantityonposition-maximum-position parameter?" + ) maximum_position: Decimal = args.biasquantityonposition_maximum_position - target_position: Decimal = args.biasquantityonposition_target_position or Decimal(0) + target_position: Decimal = ( + args.biasquantityonposition_target_position or Decimal(0) + ) return BiasQuantityOnPositionElement(maximum_position, target_position) - def process_order_pair(self, context: mango.Context, model_state: ModelState, index: int, buy: typing.Optional[mango.Order], sell: typing.Optional[mango.Order]) -> typing.Tuple[typing.Optional[mango.Order], typing.Optional[mango.Order]]: + def process_order_pair( + self, + context: mango.Context, + model_state: ModelState, + index: int, + buy: typing.Optional[mango.Order], + sell: typing.Optional[mango.Order], + ) -> typing.Tuple[typing.Optional[mango.Order], typing.Optional[mango.Order]]: if buy is not None and sell is not None: total_quantity = buy.quantity + sell.quantity current_position = model_state.inventory.base.value # BUY adjustment formula from the spreadsheet: # =MIN((A2+B2), MAX(0, ((D2-(C2-E2))/D2)*A2)) - biased_buy_quantity = ((self.maximum_position - (current_position - self.target_position) - ) / self.maximum_position) * buy.quantity - clamped_biased_buy_quantity = min(total_quantity, max(Decimal(0), biased_buy_quantity)) + biased_buy_quantity = ( + (self.maximum_position - (current_position - self.target_position)) + / self.maximum_position + ) * buy.quantity + clamped_biased_buy_quantity = min( + total_quantity, max(Decimal(0), biased_buy_quantity) + ) if buy.quantity != clamped_biased_buy_quantity: new_buy = buy.with_quantity(clamped_biased_buy_quantity) - buy_bias_description = "BUY more" if clamped_biased_buy_quantity > buy.quantity else "BUY less" - self._logger.debug(f"""BUY order change - maximum position {self.maximum_position} with current position {current_position} and target position {self.target_position} creates a {buy_bias_description} bias: + buy_bias_description = ( + "BUY more" + if clamped_biased_buy_quantity > buy.quantity + else "BUY less" + ) + self._logger.debug( + f"""BUY order change - maximum position {self.maximum_position} with current position {current_position} and target position {self.target_position} creates a {buy_bias_description} bias: Old: {buy} - New: {new_buy}""") + New: {new_buy}""" + ) buy = new_buy # SELL adjustment formula from the spreadsheet: # =MIN((A2+B2), MAX(0, (2-((D2-(C2-E2))/D2))*B2)) - biased_sell_quantity = (2 - ((self.maximum_position - (current_position - self.target_position) - ) / self.maximum_position)) * sell.quantity - clamped_biased_sell_quantity = min(total_quantity, max(Decimal(0), biased_sell_quantity)) + biased_sell_quantity = ( + 2 + - ( + (self.maximum_position - (current_position - self.target_position)) + / self.maximum_position + ) + ) * sell.quantity + clamped_biased_sell_quantity = min( + total_quantity, max(Decimal(0), biased_sell_quantity) + ) if sell.quantity != clamped_biased_sell_quantity: new_sell = sell.with_quantity(clamped_biased_sell_quantity) - sell_bias_description = "SELL more" if clamped_biased_sell_quantity > sell.quantity else "SELL less" - self._logger.debug(f"""SELL order change - maximum position {self.maximum_position} with current position {current_position} and target position {self.target_position} creates a {sell_bias_description} bias: + sell_bias_description = ( + "SELL more" + if clamped_biased_sell_quantity > sell.quantity + else "SELL less" + ) + self._logger.debug( + f"""SELL order change - maximum position {self.maximum_position} with current position {current_position} and target position {self.target_position} creates a {sell_bias_description} bias: Old: {sell} - New: {new_sell}""") + New: {new_sell}""" + ) sell = new_sell return buy, sell diff --git a/mango/marketmaking/orderchain/biasquoteelement.py b/mango/marketmaking/orderchain/biasquoteelement.py index 7d207b1..e26d8f2 100644 --- a/mango/marketmaking/orderchain/biasquoteelement.py +++ b/mango/marketmaking/orderchain/biasquoteelement.py @@ -37,17 +37,32 @@ class BiasQuoteElement(PairwiseElement): @staticmethod def add_command_line_parameters(parser: argparse.ArgumentParser) -> None: - parser.add_argument("--biasquote-factor", type=Decimal, action="append", - help="bias factor to apply to quotes. Prices will be multiplied by this factor, so a number less than 1 will reduce prices and a number greater than 1 will increase prices. For example, use 1.001 to increase prices by 10 bips. Can be specified multiple times to apply to different levels.") + parser.add_argument( + "--biasquote-factor", + type=Decimal, + action="append", + help="bias factor to apply to quotes. Prices will be multiplied by this factor, so a number less than 1 will reduce prices and a number greater than 1 will increase prices. For example, use 1.001 to increase prices by 10 bips. Can be specified multiple times to apply to different levels.", + ) @staticmethod def from_command_line_parameters(args: argparse.Namespace) -> "BiasQuoteElement": bias_factors: typing.Sequence[Decimal] = args.biasquote_factor return BiasQuoteElement(bias_factors or [Decimal(1)]) - def process_order_pair(self, context: mango.Context, model_state: ModelState, index: int, buy: typing.Optional[mango.Order], sell: typing.Optional[mango.Order]) -> typing.Tuple[typing.Optional[mango.Order], typing.Optional[mango.Order]]: + def process_order_pair( + self, + context: mango.Context, + model_state: ModelState, + index: int, + buy: typing.Optional[mango.Order], + sell: typing.Optional[mango.Order], + ) -> typing.Tuple[typing.Optional[mango.Order], typing.Optional[mango.Order]]: # If no bias is explicitly specified for this element, just use the last specified bias. - bias_factor: Decimal = self.bias_factors[index] if index < len(self.bias_factors) else self.bias_factors[-1] + bias_factor: Decimal = ( + self.bias_factors[index] + if index < len(self.bias_factors) + else self.bias_factors[-1] + ) bias_description = "BUY more" if bias_factor > 1 else "SELL more" if bias_factor == 1: @@ -59,16 +74,20 @@ class BiasQuoteElement(PairwiseElement): if buy is not None: new_buy_price: Decimal = buy.price * bias_factor new_buy = buy.with_price(new_buy_price) - self._logger.debug(f"""Order change - bias factor of {bias_factor} shifted price to {bias_description}: + self._logger.debug( + f"""Order change - bias factor of {bias_factor} shifted price to {bias_description}: Old: {buy} - New: {new_buy}""") + New: {new_buy}""" + ) if sell is not None: new_sell_price: Decimal = sell.price * bias_factor new_sell = sell.with_price(new_sell_price) - self._logger.debug(f"""Order change - bias factor of {bias_factor} shifted price to {bias_description}: + self._logger.debug( + f"""Order change - bias factor of {bias_factor} shifted price to {bias_description}: Old: {sell} - New: {new_sell}""") + New: {new_sell}""" + ) return new_buy, new_sell diff --git a/mango/marketmaking/orderchain/biasquoteonpositionelement.py b/mango/marketmaking/orderchain/biasquoteonpositionelement.py index 8a8480a..1de619b 100644 --- a/mango/marketmaking/orderchain/biasquoteonpositionelement.py +++ b/mango/marketmaking/orderchain/biasquoteonpositionelement.py @@ -37,17 +37,32 @@ class BiasQuoteOnPositionElement(PairwiseElement): @staticmethod def add_command_line_parameters(parser: argparse.ArgumentParser) -> None: - parser.add_argument("--biasquoteonposition-bias", type=Decimal, action="append", - help="bias to apply to quotes based on inventory position") + parser.add_argument( + "--biasquoteonposition-bias", + type=Decimal, + action="append", + help="bias to apply to quotes based on inventory position", + ) @staticmethod - def from_command_line_parameters(args: argparse.Namespace) -> "BiasQuoteOnPositionElement": + def from_command_line_parameters( + args: argparse.Namespace, + ) -> "BiasQuoteOnPositionElement": biases: typing.Sequence[Decimal] = args.biasquoteonposition_bias or [Decimal(0)] return BiasQuoteOnPositionElement(biases) - def process_order_pair(self, context: mango.Context, model_state: ModelState, index: int, buy: typing.Optional[mango.Order], sell: typing.Optional[mango.Order]) -> typing.Tuple[typing.Optional[mango.Order], typing.Optional[mango.Order]]: + def process_order_pair( + self, + context: mango.Context, + model_state: ModelState, + index: int, + buy: typing.Optional[mango.Order], + sell: typing.Optional[mango.Order], + ) -> typing.Tuple[typing.Optional[mango.Order], typing.Optional[mango.Order]]: # If no bias is explicitly specified for this element, just use the last specified bias. - bias: Decimal = self.biases[index] if index < len(self.biases) else self.biases[-1] + bias: Decimal = ( + self.biases[index] if index < len(self.biases) else self.biases[-1] + ) if bias == 0: # Zero bias results in no changes to orders. return buy, sell @@ -71,15 +86,19 @@ class BiasQuoteOnPositionElement(PairwiseElement): # So if my standard size I'm quoting is 0.0002 BTC, my current position is +0.0010 BTC, and pos_lean # is -0.0001, you would move your quotes down by 0.0005 (or 5bps) # (Private chat link: https://discord.com/channels/@me/832570058861314048/878343278523723787) - def bias_order(self, order: mango.Order, inventory_bias: Decimal, base_inventory_value: Decimal) -> mango.Order: + def bias_order( + self, order: mango.Order, inventory_bias: Decimal, base_inventory_value: Decimal + ) -> mango.Order: bias_factor = inventory_bias * -1 bias = 1 + ((base_inventory_value / order.quantity) * bias_factor) new_price: Decimal = order.price * bias new_order: mango.Order = order.with_price(new_price) bias_description = "BUY more" if bias > 1 else "SELL more" - self._logger.debug(f"""Order change - bias {inventory_bias} on inventory {base_inventory_value} / {order.quantity} creates a ({bias_description}) bias factor of {bias}: + self._logger.debug( + f"""Order change - bias {inventory_bias} on inventory {base_inventory_value} / {order.quantity} creates a ({bias_description}) bias factor of {bias}: Old: {order} - New: {new_order}""") + New: {new_order}""" + ) return new_order def __str__(self) -> str: diff --git a/mango/marketmaking/orderchain/chain.py b/mango/marketmaking/orderchain/chain.py index 211c4d4..ad25aca 100644 --- a/mango/marketmaking/orderchain/chain.py +++ b/mango/marketmaking/orderchain/chain.py @@ -35,7 +35,9 @@ class Chain: self._logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.elements: typing.Sequence[Element] = elements - def process(self, context: mango.Context, model_state: ModelState) -> typing.Sequence[mango.Order]: + def process( + self, context: mango.Context, model_state: ModelState + ) -> typing.Sequence[mango.Order]: orders: typing.Sequence[mango.Order] = [] for element in self.elements: orders = element.process(context, model_state, orders) diff --git a/mango/marketmaking/orderchain/chainbuilder.py b/mango/marketmaking/orderchain/chainbuilder.py index 583a207..266a542 100644 --- a/mango/marketmaking/orderchain/chainbuilder.py +++ b/mango/marketmaking/orderchain/chainbuilder.py @@ -39,7 +39,7 @@ _DEFAULT_CHAIN = [ "minimumcharge", "biasquoteonposition", "preventpostonlycrossingbook", - "roundtolotsize" + "roundtolotsize", ] @@ -51,8 +51,13 @@ _DEFAULT_CHAIN = [ class ChainBuilder: @staticmethod def add_command_line_parameters(parser: argparse.ArgumentParser) -> None: - parser.add_argument("--chain", type=str, action="append", default=[], - help="The specific order chain elements to use instead of the default chain") + parser.add_argument( + "--chain", + type=str, + action="append", + default=[], + help="The specific order chain elements to use instead of the default chain", + ) # OrderType is used by multiple elements so specify it here rather than have them fighting over which # one specifies it. # Now add args for all the elements. diff --git a/mango/marketmaking/orderchain/confidenceintervalelement.py b/mango/marketmaking/orderchain/confidenceintervalelement.py index 4415790..089f819 100644 --- a/mango/marketmaking/orderchain/confidenceintervalelement.py +++ b/mango/marketmaking/orderchain/confidenceintervalelement.py @@ -29,38 +29,68 @@ from ...modelstate import ModelState # size ratio but with a spread based on the confidence in the oracle price. # class ConfidenceIntervalElement(Element): - def __init__(self, order_type: mango.OrderType, position_size_ratio: Decimal, confidence_interval_levels: typing.Sequence[Decimal]) -> None: + def __init__( + self, + order_type: mango.OrderType, + position_size_ratio: Decimal, + confidence_interval_levels: typing.Sequence[Decimal], + ) -> None: super().__init__() self.order_type: mango.OrderType = order_type self.position_size_ratio: Decimal = position_size_ratio - self.confidence_interval_levels: typing.Sequence[Decimal] = confidence_interval_levels + self.confidence_interval_levels: typing.Sequence[ + Decimal + ] = confidence_interval_levels @staticmethod def add_command_line_parameters(parser: argparse.ArgumentParser) -> None: - parser.add_argument("--confidenceinterval-level", type=Decimal, action="append", - help="the levels of weighting to apply to the confidence interval from the oracle: e.g. 1 - use the oracle confidence interval as the spread, 2 (risk averse, default) - multiply the oracle confidence interval by 2 to get the spread, 0.5 (aggressive) halve the oracle confidence interval to get the spread (can be specified multiple times to give multiple levels)") - parser.add_argument("--confidenceinterval-position-size-ratio", type=Decimal, - help="fraction of the token inventory to be bought or sold in each order") + parser.add_argument( + "--confidenceinterval-level", + type=Decimal, + action="append", + help="the levels of weighting to apply to the confidence interval from the oracle: e.g. 1 - use the oracle confidence interval as the spread, 2 (risk averse, default) - multiply the oracle confidence interval by 2 to get the spread, 0.5 (aggressive) halve the oracle confidence interval to get the spread (can be specified multiple times to give multiple levels)", + ) + parser.add_argument( + "--confidenceinterval-position-size-ratio", + type=Decimal, + help="fraction of the token inventory to be bought or sold in each order", + ) @staticmethod - def from_command_line_parameters(args: argparse.Namespace) -> "ConfidenceIntervalElement": - if args.confidenceinterval_position_size_ratio is None or args.confidenceinterval_position_size_ratio == 0: + def from_command_line_parameters( + args: argparse.Namespace, + ) -> "ConfidenceIntervalElement": + if ( + args.confidenceinterval_position_size_ratio is None + or args.confidenceinterval_position_size_ratio == 0 + ): raise Exception("No position-size ratio specified.") order_type: mango.OrderType = args.order_type position_size_ratio: Decimal = args.confidenceinterval_position_size_ratio - confidence_interval_levels: typing.Sequence[Decimal] = args.confidenceinterval_level + confidence_interval_levels: typing.Sequence[ + Decimal + ] = args.confidenceinterval_level if len(confidence_interval_levels) == 0: confidence_interval_levels = [Decimal(2)] - return ConfidenceIntervalElement(order_type, position_size_ratio, confidence_interval_levels) + return ConfidenceIntervalElement( + order_type, position_size_ratio, confidence_interval_levels + ) - def process(self, context: mango.Context, model_state: ModelState, orders: typing.Sequence[mango.Order]) -> typing.Sequence[mango.Order]: + def process( + self, + context: mango.Context, + model_state: ModelState, + orders: typing.Sequence[mango.Order], + ) -> typing.Sequence[mango.Order]: price: mango.Price = model_state.price if price.source.supports.has_feature(mango.SupportedOracleFeature.CONFIDENCE): raise Exception(f"Price does not support confidence interval: {price}") - quote_value_to_risk = model_state.inventory.available_collateral.value * self.position_size_ratio + quote_value_to_risk = ( + model_state.inventory.available_collateral.value * self.position_size_ratio + ) position_size = quote_value_to_risk / price.mid_price new_orders: typing.List[mango.Order] = [] @@ -69,13 +99,23 @@ class ConfidenceIntervalElement(Element): bid: Decimal = price.mid_price - charge ask: Decimal = price.mid_price + charge - bid_order = mango.Order.from_basic_info(mango.Side.BUY, price=bid, - quantity=position_size, order_type=self.order_type) - ask_order = mango.Order.from_basic_info(mango.Side.SELL, price=ask, - quantity=position_size, order_type=self.order_type) - self._logger.debug(f"""Desired orders: + bid_order = mango.Order.from_basic_info( + mango.Side.BUY, + price=bid, + quantity=position_size, + order_type=self.order_type, + ) + ask_order = mango.Order.from_basic_info( + mango.Side.SELL, + price=ask, + quantity=position_size, + order_type=self.order_type, + ) + self._logger.debug( + f"""Desired orders: Bid: {bid_order} - Ask: {ask_order}""") + Ask: {ask_order}""" + ) new_orders += [bid_order, ask_order] new_orders.sort(key=lambda ord: ord.price, reverse=True) @@ -83,10 +123,14 @@ class ConfidenceIntervalElement(Element): top_bid = model_state.top_bid top_ask = model_state.top_ask - self._logger.debug(f"""Initial desired orders - spread {model_state.spread} ({top_bid.price if top_bid else None} / {top_ask.price if top_ask else None}): - {order_text}""") + self._logger.debug( + f"""Initial desired orders - spread {model_state.spread} ({top_bid.price if top_bid else None} / {top_ask.price if top_ask else None}): + {order_text}""" + ) return new_orders def __str__(self) -> str: - confidence_interval_levels = ", ".join(map(str, self.confidence_interval_levels)) or "None" + confidence_interval_levels = ( + ", ".join(map(str, self.confidence_interval_levels)) or "None" + ) return f"ยซ ConfidenceIntervalElement {self.order_type} - position size: {self.position_size_ratio}, confidence interval levels: {confidence_interval_levels} ยป" diff --git a/mango/marketmaking/orderchain/element.py b/mango/marketmaking/orderchain/element.py index 96eb192..084275d 100644 --- a/mango/marketmaking/orderchain/element.py +++ b/mango/marketmaking/orderchain/element.py @@ -40,11 +40,20 @@ class Element(metaclass=abc.ABCMeta): @staticmethod def from_command_line_parameters(args: argparse.Namespace) -> "Element": - raise NotImplementedError("Element.from_command_line_parameters() is not implemented on the base type.") + raise NotImplementedError( + "Element.from_command_line_parameters() is not implemented on the base type." + ) @abc.abstractmethod - def process(self, context: mango.Context, model_state: ModelState, orders: typing.Sequence[mango.Order]) -> typing.Sequence[mango.Order]: - raise NotImplementedError("Element.process() is not implemented on the base type.") + def process( + self, + context: mango.Context, + model_state: ModelState, + orders: typing.Sequence[mango.Order], + ) -> typing.Sequence[mango.Order]: + raise NotImplementedError( + "Element.process() is not implemented on the base type." + ) def __repr__(self) -> str: return f"{self}" diff --git a/mango/marketmaking/orderchain/fixedpositionsizeelement.py b/mango/marketmaking/orderchain/fixedpositionsizeelement.py index cdea26c..b1106a4 100644 --- a/mango/marketmaking/orderchain/fixedpositionsizeelement.py +++ b/mango/marketmaking/orderchain/fixedpositionsizeelement.py @@ -35,35 +35,60 @@ class FixedPositionSizeElement(PairwiseElement): @staticmethod def add_command_line_parameters(parser: argparse.ArgumentParser) -> None: - parser.add_argument("--fixedpositionsize-value", type=Decimal, action="append", - help="fixed value to use as the position size. Can be specified multiple times for multiple levels of BUYs and SELLs.") + parser.add_argument( + "--fixedpositionsize-value", + type=Decimal, + action="append", + help="fixed value to use as the position size. Can be specified multiple times for multiple levels of BUYs and SELLs.", + ) @staticmethod - def from_command_line_parameters(args: argparse.Namespace) -> "FixedPositionSizeElement": + def from_command_line_parameters( + args: argparse.Namespace, + ) -> "FixedPositionSizeElement": if args.fixedpositionsize_value is None: - raise Exception("No position-size value specified. Try the --fixedpositionsize-value parameter?") + raise Exception( + "No position-size value specified. Try the --fixedpositionsize-value parameter?" + ) position_sizes: typing.Sequence[Decimal] = args.fixedpositionsize_value return FixedPositionSizeElement(position_sizes) - def process_order_pair(self, context: mango.Context, model_state: ModelState, index: int, buy: typing.Optional[mango.Order], sell: typing.Optional[mango.Order]) -> typing.Tuple[typing.Optional[mango.Order], typing.Optional[mango.Order]]: + def process_order_pair( + self, + context: mango.Context, + model_state: ModelState, + index: int, + buy: typing.Optional[mango.Order], + sell: typing.Optional[mango.Order], + ) -> typing.Tuple[typing.Optional[mango.Order], typing.Optional[mango.Order]]: # If no position size is explicitly specified for this element, just use the last specified size. - size: Decimal = self.position_sizes[index] if index < len(self.position_sizes) else self.position_sizes[-1] + size: Decimal = ( + self.position_sizes[index] + if index < len(self.position_sizes) + else self.position_sizes[-1] + ) new_buy: typing.Optional[mango.Order] = None new_sell: typing.Optional[mango.Order] = None if buy is not None: new_buy = buy.with_quantity(size) - self._logger.debug(f"""Order change - using fixed position size of {size}: + self._logger.debug( + f"""Order change - using fixed position size of {size}: Old: {buy} - New: {new_buy}""") + New: {new_buy}""" + ) if sell is not None: new_sell = sell.with_quantity(size) - self._logger.debug(f"""Order change - using fixed position size of {size}: + self._logger.debug( + f"""Order change - using fixed position size of {size}: Old: {sell} - New: {new_sell}""") + New: {new_sell}""" + ) return new_buy, new_sell def __str__(self) -> str: - return f"ยซ FixedPositionSizeElement using position sizes: {self.position_sizes} ยป" + return ( + f"ยซ FixedPositionSizeElement using position sizes: {self.position_sizes} ยป" + ) diff --git a/mango/marketmaking/orderchain/fixedspreadelement.py b/mango/marketmaking/orderchain/fixedspreadelement.py index b9a6aa8..ba59018 100644 --- a/mango/marketmaking/orderchain/fixedspreadelement.py +++ b/mango/marketmaking/orderchain/fixedspreadelement.py @@ -35,20 +35,35 @@ class FixedSpreadElement(PairwiseElement): @staticmethod def add_command_line_parameters(parser: argparse.ArgumentParser) -> None: - parser.add_argument("--fixedspread-value", type=Decimal, action="append", - help="fixed value to apply to the mid-price to create the BUY and SELL price. Can be specified multiple times for multiple levels of BUYs and SELLs.") + parser.add_argument( + "--fixedspread-value", + type=Decimal, + action="append", + help="fixed value to apply to the mid-price to create the BUY and SELL price. Can be specified multiple times for multiple levels of BUYs and SELLs.", + ) @staticmethod def from_command_line_parameters(args: argparse.Namespace) -> "FixedSpreadElement": if args.fixedspread_value is None: - raise Exception("No spread value specified. Try the --fixedspread-value parameter?") + raise Exception( + "No spread value specified. Try the --fixedspread-value parameter?" + ) spreads: typing.Sequence[Decimal] = args.fixedspread_value return FixedSpreadElement(spreads) - def process_order_pair(self, context: mango.Context, model_state: ModelState, index: int, buy: typing.Optional[mango.Order], sell: typing.Optional[mango.Order]) -> typing.Tuple[typing.Optional[mango.Order], typing.Optional[mango.Order]]: + def process_order_pair( + self, + context: mango.Context, + model_state: ModelState, + index: int, + buy: typing.Optional[mango.Order], + sell: typing.Optional[mango.Order], + ) -> typing.Tuple[typing.Optional[mango.Order], typing.Optional[mango.Order]]: # If no spread is explicitly specified for this element, just use the last specified spread. - spread: Decimal = self.spreads[index] if index < len(self.spreads) else self.spreads[-1] + spread: Decimal = ( + self.spreads[index] if index < len(self.spreads) else self.spreads[-1] + ) half_spread: Decimal = spread / 2 price: mango.Price = model_state.price new_buy: typing.Optional[mango.Order] = None @@ -56,16 +71,20 @@ class FixedSpreadElement(PairwiseElement): if buy is not None: new_buy_price: Decimal = price.mid_price - half_spread new_buy = buy.with_price(new_buy_price) - self._logger.debug(f"""Order change - using fixed spread of {spread:,.8f} - new BUY price {new_buy_price:,.8f} is {half_spread:,.8f} from mid price {price.mid_price:,.8f}: + self._logger.debug( + f"""Order change - using fixed spread of {spread:,.8f} - new BUY price {new_buy_price:,.8f} is {half_spread:,.8f} from mid price {price.mid_price:,.8f}: Old: {buy} - New: {new_buy}""") + New: {new_buy}""" + ) if sell is not None: new_sell_price: Decimal = price.mid_price + half_spread new_sell = sell.with_price(new_sell_price) - self._logger.debug(f"""Order change - using fixed spread of {spread:,.8f} - new SELL price {new_sell_price:,.8f} is {half_spread:,.8f} from mid price {price.mid_price:,.8f}: + self._logger.debug( + f"""Order change - using fixed spread of {spread:,.8f} - new SELL price {new_sell_price:,.8f} is {half_spread:,.8f} from mid price {price.mid_price:,.8f}: Old: {sell} - New: {new_sell}""") + New: {new_sell}""" + ) return new_buy, new_sell diff --git a/mango/marketmaking/orderchain/maximumquantityelement.py b/mango/marketmaking/orderchain/maximumquantityelement.py index fff1f69..3f2d18d 100644 --- a/mango/marketmaking/orderchain/maximumquantityelement.py +++ b/mango/marketmaking/orderchain/maximumquantityelement.py @@ -37,33 +37,55 @@ class MaximumQuantityElement(Element): @staticmethod def add_command_line_parameters(parser: argparse.ArgumentParser) -> None: - parser.add_argument("--maximumquantity-size", type=Decimal, - help="the maximum permitted order quantity") - parser.add_argument("--maximumquantity-remove", action="store_true", default=False, - help="remove an order that has too big a quantity (default is to reduce order quantity to maximum)") + parser.add_argument( + "--maximumquantity-size", + type=Decimal, + help="the maximum permitted order quantity", + ) + parser.add_argument( + "--maximumquantity-remove", + action="store_true", + default=False, + help="remove an order that has too big a quantity (default is to reduce order quantity to maximum)", + ) @staticmethod - def from_command_line_parameters(args: argparse.Namespace) -> "MaximumQuantityElement": + def from_command_line_parameters( + args: argparse.Namespace, + ) -> "MaximumQuantityElement": if args.maximumquantity_size is None: - raise Exception("No maximum size specified. Try the --maximumquantity-size parameter?") + raise Exception( + "No maximum size specified. Try the --maximumquantity-size parameter?" + ) - return MaximumQuantityElement(args.maximumquantity_size, bool(args.maximumquantity_remove)) + return MaximumQuantityElement( + args.maximumquantity_size, bool(args.maximumquantity_remove) + ) - def process(self, context: mango.Context, model_state: ModelState, orders: typing.Sequence[mango.Order]) -> typing.Sequence[mango.Order]: + def process( + self, + context: mango.Context, + model_state: ModelState, + orders: typing.Sequence[mango.Order], + ) -> typing.Sequence[mango.Order]: new_orders: typing.List[mango.Order] = [] for order in orders: if order.quantity < self.maximum_quantity: new_orders += [order] else: if self.remove: - self._logger.debug(f"""Order change - order quantity is greater than maximum of {self.maximum_quantity} so removing: + self._logger.debug( + f"""Order change - order quantity is greater than maximum of {self.maximum_quantity} so removing: Old: {order} - New: None""") + New: None""" + ) else: new_order: mango.Order = order.with_quantity(self.maximum_quantity) - self._logger.debug(f"""Order change - order quantity is greater than maximum of {self.maximum_quantity} so changing order quantity to {self.maximum_quantity}: + self._logger.debug( + f"""Order change - order quantity is greater than maximum of {self.maximum_quantity} so changing order quantity to {self.maximum_quantity}: Old: {order} - New: {new_order}""") + New: {new_order}""" + ) new_orders += [new_order] return new_orders diff --git a/mango/marketmaking/orderchain/minimumchargeelement.py b/mango/marketmaking/orderchain/minimumchargeelement.py index af1a2b9..02d01df 100644 --- a/mango/marketmaking/orderchain/minimumchargeelement.py +++ b/mango/marketmaking/orderchain/minimumchargeelement.py @@ -38,21 +38,43 @@ class MinimumChargeElement(PairwiseElement): @staticmethod def add_command_line_parameters(parser: argparse.ArgumentParser) -> None: - parser.add_argument("--minimumcharge-ratio", type=Decimal, action="append", - help="minimum fraction of the price to be accept as a spread") - parser.add_argument("--minimumcharge-from-bid-ask", action="store_true", default=False, - help="calculate minimum charge from bid or ask, not mid price (default: False, which will use the mid price)") + parser.add_argument( + "--minimumcharge-ratio", + type=Decimal, + action="append", + help="minimum fraction of the price to be accept as a spread", + ) + parser.add_argument( + "--minimumcharge-from-bid-ask", + action="store_true", + default=False, + help="calculate minimum charge from bid or ask, not mid price (default: False, which will use the mid price)", + ) @staticmethod - def from_command_line_parameters(args: argparse.Namespace) -> "MinimumChargeElement": - minimumcharge_ratios: typing.Sequence[Decimal] = args.minimumcharge_ratio or [Decimal("0.0005")] + def from_command_line_parameters( + args: argparse.Namespace, + ) -> "MinimumChargeElement": + minimumcharge_ratios: typing.Sequence[Decimal] = args.minimumcharge_ratio or [ + Decimal("0.0005") + ] minimumcharge_from_bid_ask: bool = args.minimumcharge_from_bid_ask return MinimumChargeElement(minimumcharge_ratios, minimumcharge_from_bid_ask) - def process_order_pair(self, context: mango.Context, model_state: ModelState, index: int, buy: typing.Optional[mango.Order], sell: typing.Optional[mango.Order]) -> typing.Tuple[typing.Optional[mango.Order], typing.Optional[mango.Order]]: + def process_order_pair( + self, + context: mango.Context, + model_state: ModelState, + index: int, + buy: typing.Optional[mango.Order], + sell: typing.Optional[mango.Order], + ) -> typing.Tuple[typing.Optional[mango.Order], typing.Optional[mango.Order]]: # If no bias is explicitly specified for this element, just use the last specified bias. - minimum_charge_ratio: Decimal = self.minimumcharge_ratios[index] if index < len( - self.minimumcharge_ratios) else self.minimumcharge_ratios[-1] + minimum_charge_ratio: Decimal = ( + self.minimumcharge_ratios[index] + if index < len(self.minimumcharge_ratios) + else self.minimumcharge_ratios[-1] + ) if minimum_charge_ratio == 0: # Zero minimum charge results in no changes to orders. return buy, sell @@ -66,26 +88,38 @@ class MinimumChargeElement(PairwiseElement): current_charge: Decimal new_price: Decimal if buy is not None: - measurement_price = model_state.price.top_bid if self.minimumcharge_from_bid_ask else model_state.price.mid_price + measurement_price = ( + model_state.price.top_bid + if self.minimumcharge_from_bid_ask + else model_state.price.mid_price + ) minimum_charge = measurement_price * minimum_charge_ratio current_charge = measurement_price - buy.price if current_charge < minimum_charge: new_price = measurement_price - minimum_charge new_buy = buy.with_price(new_price) - self._logger.debug(f"""Order change - old BUY price {buy.price:,.8f} distance from {measurement_price:,.8f} would return {current_charge:,.8f} which is less than minimum charge {minimum_charge:,.8f}: + self._logger.debug( + f"""Order change - old BUY price {buy.price:,.8f} distance from {measurement_price:,.8f} would return {current_charge:,.8f} which is less than minimum charge {minimum_charge:,.8f}: Old: {buy} - New: {new_buy}""") + New: {new_buy}""" + ) if sell is not None: - measurement_price = model_state.price.top_ask if self.minimumcharge_from_bid_ask else model_state.price.mid_price + measurement_price = ( + model_state.price.top_ask + if self.minimumcharge_from_bid_ask + else model_state.price.mid_price + ) minimum_charge = measurement_price * minimum_charge_ratio current_charge = sell.price - measurement_price if current_charge < minimum_charge: new_price = measurement_price + minimum_charge new_sell = sell.with_price(new_price) - self._logger.debug(f"""Order change - old SELL price {sell.price:,.8f} distance from {measurement_price:,.8f} would return {current_charge:,.8f} which is less than minimum charge {minimum_charge:,.8f}: + self._logger.debug( + f"""Order change - old SELL price {sell.price:,.8f} distance from {measurement_price:,.8f} would return {current_charge:,.8f} which is less than minimum charge {minimum_charge:,.8f}: Old: {sell} - New: {new_sell}""") + New: {new_sell}""" + ) return new_buy, new_sell diff --git a/mango/marketmaking/orderchain/minimumquantityelement.py b/mango/marketmaking/orderchain/minimumquantityelement.py index 9952951..058c2da 100644 --- a/mango/marketmaking/orderchain/minimumquantityelement.py +++ b/mango/marketmaking/orderchain/minimumquantityelement.py @@ -37,33 +37,56 @@ class MinimumQuantityElement(Element): @staticmethod def add_command_line_parameters(parser: argparse.ArgumentParser) -> None: - parser.add_argument("--minimumquantity-size", type=Decimal, default=Decimal(1), - help="the minimum permitted quantity") - parser.add_argument("--minimumquantity-remove", action="store_true", default=False, - help="remove an order that has too small a quantity (default is to increase order quantity to minimum)") + parser.add_argument( + "--minimumquantity-size", + type=Decimal, + default=Decimal(1), + help="the minimum permitted quantity", + ) + parser.add_argument( + "--minimumquantity-remove", + action="store_true", + default=False, + help="remove an order that has too small a quantity (default is to increase order quantity to minimum)", + ) @staticmethod - def from_command_line_parameters(args: argparse.Namespace) -> "MinimumQuantityElement": + def from_command_line_parameters( + args: argparse.Namespace, + ) -> "MinimumQuantityElement": if args.minimumquantity_size is None: - raise Exception("No minimum size specified. Try the --minimumquantity-size parameter?") + raise Exception( + "No minimum size specified. Try the --minimumquantity-size parameter?" + ) - return MinimumQuantityElement(args.minimumquantity_size, bool(args.minimumquantity_remove)) + return MinimumQuantityElement( + args.minimumquantity_size, bool(args.minimumquantity_remove) + ) - def process(self, context: mango.Context, model_state: ModelState, orders: typing.Sequence[mango.Order]) -> typing.Sequence[mango.Order]: + def process( + self, + context: mango.Context, + model_state: ModelState, + orders: typing.Sequence[mango.Order], + ) -> typing.Sequence[mango.Order]: new_orders: typing.List[mango.Order] = [] for order in orders: if order.quantity > self.minimum_quantity: new_orders += [order] else: if self.remove: - self._logger.debug(f"""Order change - order quantity is less than minimum of {self.minimum_quantity} so removing: + self._logger.debug( + f"""Order change - order quantity is less than minimum of {self.minimum_quantity} so removing: Old: {order} - New: None""") + New: None""" + ) else: new_order: mango.Order = order.with_quantity(self.minimum_quantity) - self._logger.debug(f"""Order change - order quantity is less than minimum of {self.minimum_quantity} so changing order quantity to {self.minimum_quantity}: + self._logger.debug( + f"""Order change - order quantity is less than minimum of {self.minimum_quantity} so changing order quantity to {self.minimum_quantity}: Old: {order} - New: {new_order}""") + New: {new_order}""" + ) new_orders += [new_order] return new_orders diff --git a/mango/marketmaking/orderchain/pairwiseelement.py b/mango/marketmaking/orderchain/pairwiseelement.py index 27805e4..0bd20a1 100644 --- a/mango/marketmaking/orderchain/pairwiseelement.py +++ b/mango/marketmaking/orderchain/pairwiseelement.py @@ -43,8 +43,17 @@ class PairwiseElement(Element, metaclass=abc.ABCMeta): def __init__(self) -> None: super().__init__() - def process_order_pair(self, context: mango.Context, model_state: ModelState, index: int, buy: typing.Optional[mango.Order], sell: typing.Optional[mango.Order]) -> typing.Tuple[typing.Optional[mango.Order], typing.Optional[mango.Order]]: - raise NotImplementedError("PairwiseElement.process_order_pair() is not implemented on the base type.") + def process_order_pair( + self, + context: mango.Context, + model_state: ModelState, + index: int, + buy: typing.Optional[mango.Order], + sell: typing.Optional[mango.Order], + ) -> typing.Tuple[typing.Optional[mango.Order], typing.Optional[mango.Order]]: + raise NotImplementedError( + "PairwiseElement.process_order_pair() is not implemented on the base type." + ) # If multiple levels are specified, we want to process them in order. # @@ -61,19 +70,34 @@ class PairwiseElement(Element, metaclass=abc.ABCMeta): # * Sort the two lists so closest to top-of-book is at index 0 # * Call process_order_pair() for each paired BUY and SELL, with the index parameter being # the index into the BUY and SELL lists. - def process(self, context: mango.Context, model_state: ModelState, orders: typing.Sequence[mango.Order]) -> typing.Sequence[mango.Order]: - buys: typing.List[mango.Order] = list([order for order in orders if order.side == mango.Side.BUY]) + def process( + self, + context: mango.Context, + model_state: ModelState, + orders: typing.Sequence[mango.Order], + ) -> typing.Sequence[mango.Order]: + buys: typing.List[mango.Order] = list( + [order for order in orders if order.side == mango.Side.BUY] + ) buys.sort(key=lambda order: order.price, reverse=True) - sells: typing.List[mango.Order] = list([order for order in orders if order.side == mango.Side.SELL]) + sells: typing.List[mango.Order] = list( + [order for order in orders if order.side == mango.Side.SELL] + ) sells.sort(key=lambda order: order.price) pair_count: int = max(len(buys), len(sells)) new_orders: typing.List[mango.Order] = [] for index in range(pair_count): - old_buy: typing.Optional[mango.Order] = buys[index] if index < len(buys) else None - old_sell: typing.Optional[mango.Order] = sells[index] if index < len(sells) else None + old_buy: typing.Optional[mango.Order] = ( + buys[index] if index < len(buys) else None + ) + old_sell: typing.Optional[mango.Order] = ( + sells[index] if index < len(sells) else None + ) - (new_buy, new_sell) = self.process_order_pair(context, model_state, index, old_buy, old_sell) + (new_buy, new_sell) = self.process_order_pair( + context, model_state, index, old_buy, old_sell + ) if new_buy is not None: new_orders += [new_buy] diff --git a/mango/marketmaking/orderchain/preventpostonlycrossingbookelement.py b/mango/marketmaking/orderchain/preventpostonlycrossingbookelement.py index cfe8d2d..7687836 100644 --- a/mango/marketmaking/orderchain/preventpostonlycrossingbookelement.py +++ b/mango/marketmaking/orderchain/preventpostonlycrossingbookelement.py @@ -37,29 +37,59 @@ class PreventPostOnlyCrossingBookElement(Element): pass @staticmethod - def from_command_line_parameters(args: argparse.Namespace) -> "PreventPostOnlyCrossingBookElement": + def from_command_line_parameters( + args: argparse.Namespace, + ) -> "PreventPostOnlyCrossingBookElement": return PreventPostOnlyCrossingBookElement() - def process(self, context: mango.Context, model_state: ModelState, orders: typing.Sequence[mango.Order]) -> typing.Sequence[mango.Order]: + def process( + self, + context: mango.Context, + model_state: ModelState, + orders: typing.Sequence[mango.Order], + ) -> typing.Sequence[mango.Order]: new_orders: typing.List[mango.Order] = [] for order in orders: if order.order_type == mango.OrderType.POST_ONLY: - top_bid: typing.Optional[Decimal] = model_state.top_bid.price if model_state.top_bid is not None else None - top_ask: typing.Optional[Decimal] = model_state.top_ask.price if model_state.top_ask is not None else None - if order.side == mango.Side.BUY and top_ask is not None and order.price >= top_ask: - new_buy_price: Decimal = top_ask - model_state.market.lot_size_converter.tick_size + top_bid: typing.Optional[Decimal] = ( + model_state.top_bid.price + if model_state.top_bid is not None + else None + ) + top_ask: typing.Optional[Decimal] = ( + model_state.top_ask.price + if model_state.top_ask is not None + else None + ) + if ( + order.side == mango.Side.BUY + and top_ask is not None + and order.price >= top_ask + ): + new_buy_price: Decimal = ( + top_ask - model_state.market.lot_size_converter.tick_size + ) new_buy: mango.Order = order.with_price(new_buy_price) - self._logger.debug(f"""Order change - would cross the orderbook {top_bid} / {top_ask}: + self._logger.debug( + f"""Order change - would cross the orderbook {top_bid} / {top_ask}: Old: {order} - New: {new_buy}""") + New: {new_buy}""" + ) new_orders += [new_buy] - elif order.side == mango.Side.SELL and top_bid is not None and order.price <= top_bid: - new_sell_price: Decimal = top_bid + model_state.market.lot_size_converter.tick_size + elif ( + order.side == mango.Side.SELL + and top_bid is not None + and order.price <= top_bid + ): + new_sell_price: Decimal = ( + top_bid + model_state.market.lot_size_converter.tick_size + ) new_sell: mango.Order = order.with_price(new_sell_price) self._logger.debug( f"""Order change - would cross the orderbook {top_bid} / {top_ask}: Old: {order} - New: {new_sell}""") + New: {new_sell}""" + ) new_orders += [new_sell] else: diff --git a/mango/marketmaking/orderchain/quotesinglesideelement.py b/mango/marketmaking/orderchain/quotesinglesideelement.py index 9539f5a..b6e7de3 100644 --- a/mango/marketmaking/orderchain/quotesinglesideelement.py +++ b/mango/marketmaking/orderchain/quotesinglesideelement.py @@ -32,24 +32,38 @@ class QuoteSingleSideElement(Element): @staticmethod def add_command_line_parameters(parser: argparse.ArgumentParser) -> None: - parser.add_argument("--quotesingleside-side", type=mango.Side, - help="the single side to quote on - if BUY, all SELLs will be removed from desired orders, if SELL, all BUYs will be removed.") + parser.add_argument( + "--quotesingleside-side", + type=mango.Side, + help="the single side to quote on - if BUY, all SELLs will be removed from desired orders, if SELL, all BUYs will be removed.", + ) @staticmethod - def from_command_line_parameters(args: argparse.Namespace) -> "QuoteSingleSideElement": + def from_command_line_parameters( + args: argparse.Namespace, + ) -> "QuoteSingleSideElement": side: mango.Side = args.quotesingleside_side return QuoteSingleSideElement(side) - def process(self, context: mango.Context, model_state: ModelState, orders: typing.Sequence[mango.Order]) -> typing.Sequence[mango.Order]: + def process( + self, + context: mango.Context, + model_state: ModelState, + orders: typing.Sequence[mango.Order], + ) -> typing.Sequence[mango.Order]: new_orders: typing.List[mango.Order] = [] for order in orders: if order.side == self.allowed: - self._logger.debug(f"""Allowing {order.side} order [allowed: {self.allowed}]: - Allowed: {order}""") + self._logger.debug( + f"""Allowing {order.side} order [allowed: {self.allowed}]: + Allowed: {order}""" + ) new_orders += [order] else: - self._logger.debug(f"""Removing {order.side} order [allowed: {self.allowed}]: - Removed: {order}""") + self._logger.debug( + f"""Removing {order.side} order [allowed: {self.allowed}]: + Removed: {order}""" + ) return new_orders diff --git a/mango/marketmaking/orderchain/ratioselement.py b/mango/marketmaking/orderchain/ratioselement.py index 2174d96..f0c46df 100644 --- a/mango/marketmaking/orderchain/ratioselement.py +++ b/mango/marketmaking/orderchain/ratioselement.py @@ -33,7 +33,13 @@ DEFAULT_POSITION_SIZE_RATIO = Decimal("0.01") # ratio and a position size ratio. # class RatiosElement(Element): - def __init__(self, order_type: mango.OrderType, spread_ratios: typing.Sequence[Decimal], position_size_ratios: typing.Sequence[Decimal], from_bid_ask: bool) -> None: + def __init__( + self, + order_type: mango.OrderType, + spread_ratios: typing.Sequence[Decimal], + position_size_ratios: typing.Sequence[Decimal], + from_bid_ask: bool, + ) -> None: super().__init__() self.order_type: mango.OrderType = order_type self.spread_ratios: typing.Sequence[Decimal] = spread_ratios @@ -41,37 +47,68 @@ class RatiosElement(Element): self.from_bid_ask: bool = from_bid_ask if len(self.spread_ratios) == 0: - raise Exception("No spread ratios specified. Try the --ratios-spread parameter?") + raise Exception( + "No spread ratios specified. Try the --ratios-spread parameter?" + ) if len(self.position_size_ratios) == 0: - raise Exception("No position-size ratios specified. Try the --ratios-position-size parameter?") + raise Exception( + "No position-size ratios specified. Try the --ratios-position-size parameter?" + ) if len(self.spread_ratios) != len(self.position_size_ratios): - raise Exception("List of spread ratios and position size ratios must be the same length.") + raise Exception( + "List of spread ratios and position size ratios must be the same length." + ) @staticmethod def add_command_line_parameters(parser: argparse.ArgumentParser) -> None: - parser.add_argument("--ratios-spread", type=Decimal, action="append", - help="ratio to apply to the mid-price to create the BUY and SELL price (can be specified multiple times but every occurrance must have a matching --position-size-ratio occurrance)") - parser.add_argument("--ratios-position-size", type=Decimal, action="append", - help="ratio to apply to the available collateral to create the position size (can be specified multiple times but every occurrance must have a matching --spread-ratio occurrance)") - parser.add_argument("--ratios-from-bid-ask", action="store_true", default=False, - help="calculate ratios from bid or ask, not mid price (default: False, which will use the mid price)") + parser.add_argument( + "--ratios-spread", + type=Decimal, + action="append", + help="ratio to apply to the mid-price to create the BUY and SELL price (can be specified multiple times but every occurrance must have a matching --position-size-ratio occurrance)", + ) + parser.add_argument( + "--ratios-position-size", + type=Decimal, + action="append", + help="ratio to apply to the available collateral to create the position size (can be specified multiple times but every occurrance must have a matching --spread-ratio occurrance)", + ) + parser.add_argument( + "--ratios-from-bid-ask", + action="store_true", + default=False, + help="calculate ratios from bid or ask, not mid price (default: False, which will use the mid price)", + ) @staticmethod def from_command_line_parameters(args: argparse.Namespace) -> "RatiosElement": order_type: mango.OrderType = args.order_type - spread_ratios: typing.Sequence[Decimal] = args.ratios_spread or [DEFAULT_SPREAD_RATIO] - position_size_ratios: typing.Sequence[Decimal] = args.ratios_position_size or [DEFAULT_POSITION_SIZE_RATIO] + spread_ratios: typing.Sequence[Decimal] = args.ratios_spread or [ + DEFAULT_SPREAD_RATIO + ] + position_size_ratios: typing.Sequence[Decimal] = args.ratios_position_size or [ + DEFAULT_POSITION_SIZE_RATIO + ] from_bid_ask: bool = args.ratios_from_bid_ask - return RatiosElement(order_type, spread_ratios, position_size_ratios, from_bid_ask) + return RatiosElement( + order_type, spread_ratios, position_size_ratios, from_bid_ask + ) - def process(self, context: mango.Context, model_state: ModelState, orders: typing.Sequence[mango.Order]) -> typing.Sequence[mango.Order]: + def process( + self, + context: mango.Context, + model_state: ModelState, + orders: typing.Sequence[mango.Order], + ) -> typing.Sequence[mango.Order]: price: mango.Price = model_state.price new_orders: typing.List[mango.Order] = [] for counter in range(len(self.spread_ratios)): position_size_ratio = self.position_size_ratios[counter] - quote_value_to_risk = model_state.inventory.available_collateral.value * position_size_ratio + quote_value_to_risk = ( + model_state.inventory.available_collateral.value * position_size_ratio + ) base_position_size = quote_value_to_risk / price.mid_price spread_ratio = self.spread_ratios[counter] @@ -84,13 +121,23 @@ class RatiosElement(Element): bid: Decimal = bid_price_base - (bid_price_base * spread_ratio) ask: Decimal = ask_price_base + (ask_price_base * spread_ratio) - bid_order = mango.Order.from_basic_info(mango.Side.BUY, price=bid, - quantity=base_position_size, order_type=self.order_type) - ask_order = mango.Order.from_basic_info(mango.Side.SELL, price=ask, - quantity=base_position_size, order_type=self.order_type) - self._logger.debug(f"""Desired orders: + bid_order = mango.Order.from_basic_info( + mango.Side.BUY, + price=bid, + quantity=base_position_size, + order_type=self.order_type, + ) + ask_order = mango.Order.from_basic_info( + mango.Side.SELL, + price=ask, + quantity=base_position_size, + order_type=self.order_type, + ) + self._logger.debug( + f"""Desired orders: Bid: {bid_order} - Ask: {ask_order}""") + Ask: {ask_order}""" + ) new_orders += [bid_order, ask_order] return new_orders diff --git a/mango/marketmaking/orderchain/roundtolotsizeelement.py b/mango/marketmaking/orderchain/roundtolotsizeelement.py index 0db3c9f..2eed5bd 100644 --- a/mango/marketmaking/orderchain/roundtolotsizeelement.py +++ b/mango/marketmaking/orderchain/roundtolotsizeelement.py @@ -36,24 +36,43 @@ class RoundToLotSizeElement(Element): pass @staticmethod - def from_command_line_parameters(args: argparse.Namespace) -> "RoundToLotSizeElement": + def from_command_line_parameters( + args: argparse.Namespace, + ) -> "RoundToLotSizeElement": return RoundToLotSizeElement() - def process(self, context: mango.Context, model_state: ModelState, orders: typing.Sequence[mango.Order]) -> typing.Sequence[mango.Order]: + def process( + self, + context: mango.Context, + model_state: ModelState, + orders: typing.Sequence[mango.Order], + ) -> typing.Sequence[mango.Order]: new_orders: typing.List[mango.Order] = [] for order in orders: - new_price: Decimal = model_state.market.lot_size_converter.round_quote(order.price) - new_quantity: Decimal = model_state.market.lot_size_converter.round_base(order.quantity) - new_order: mango.Order = order.with_price(new_price).with_quantity(new_quantity) + new_price: Decimal = model_state.market.lot_size_converter.round_quote( + order.price + ) + new_quantity: Decimal = model_state.market.lot_size_converter.round_base( + order.quantity + ) + new_order: mango.Order = order.with_price(new_price).with_quantity( + new_quantity + ) if new_order.price == 0 or new_order.quantity == 0: - self._logger.debug(f"""Order removed - price or quantity rounded to zero: + self._logger.debug( + f"""Order removed - price or quantity rounded to zero: Old: {order} - New: {new_order}""") - elif (order.price != new_order.price) or (order.quantity != new_order.quantity): + New: {new_order}""" + ) + elif (order.price != new_order.price) or ( + order.quantity != new_order.quantity + ): new_orders += [new_order] - self._logger.debug(f"""Order change - price and quantity now aligned to lot size: + self._logger.debug( + f"""Order change - price and quantity now aligned to lot size: Old: {order} - New: {new_order}""") + New: {new_order}""" + ) else: new_orders += [order] diff --git a/mango/marketmaking/orderchain/topofbookelement.py b/mango/marketmaking/orderchain/topofbookelement.py index d2e916a..0f15390 100644 --- a/mango/marketmaking/orderchain/topofbookelement.py +++ b/mango/marketmaking/orderchain/topofbookelement.py @@ -41,46 +41,69 @@ class TopOfBookElement(Element): @staticmethod def add_command_line_parameters(parser: argparse.ArgumentParser) -> None: - parser.add_argument("--topofbook-adjustment-ticks", type=Decimal, default=Decimal(1), - help="number of ticks above/below the previous top-of-book to place the order. Default is 1 tick - 1 tick below for a SELL, 1 tick above for a BUY. Use 0 to specify placing the order AT the current best.") + parser.add_argument( + "--topofbook-adjustment-ticks", + type=Decimal, + default=Decimal(1), + help="number of ticks above/below the previous top-of-book to place the order. Default is 1 tick - 1 tick below for a SELL, 1 tick above for a BUY. Use 0 to specify placing the order AT the current best.", + ) @staticmethod def from_command_line_parameters(args: argparse.Namespace) -> "TopOfBookElement": return TopOfBookElement(args.topofbook_adjustment_ticks) - def _best_order_from_someone_else(self, orders: typing.Sequence[mango.Order], owner: PublicKey) -> typing.Optional[mango.Order]: + def _best_order_from_someone_else( + self, orders: typing.Sequence[mango.Order], owner: PublicKey + ) -> typing.Optional[mango.Order]: for order in orders: if order.owner != owner: # Success! return order return None - def process(self, context: mango.Context, model_state: ModelState, orders: typing.Sequence[mango.Order]) -> typing.Sequence[mango.Order]: + def process( + self, + context: mango.Context, + model_state: ModelState, + orders: typing.Sequence[mango.Order], + ) -> typing.Sequence[mango.Order]: new_orders: typing.List[mango.Order] = [] - adjustment: Decimal = self.adjustment_ticks * model_state.market.lot_size_converter.tick_size + adjustment: Decimal = ( + self.adjustment_ticks * model_state.market.lot_size_converter.tick_size + ) for order in orders: new_price: typing.Optional[Decimal] = None if order.side == mango.Side.BUY: - place_above: typing.Optional[mango.Order] = self._best_order_from_someone_else( - model_state.bids, model_state.order_owner) + place_above: typing.Optional[ + mango.Order + ] = self._best_order_from_someone_else( + model_state.bids, model_state.order_owner + ) if place_above is not None: new_price = place_above.price + adjustment else: - place_below: typing.Optional[mango.Order] = self._best_order_from_someone_else( - model_state.asks, model_state.order_owner) + place_below: typing.Optional[ + mango.Order + ] = self._best_order_from_someone_else( + model_state.asks, model_state.order_owner + ) if place_below is not None: new_price = place_below.price - adjustment if new_price is None: - self._logger.debug(f"""Order change - no acceptable price from anyone else so leaving it as it is: + self._logger.debug( + f"""Order change - no acceptable price from anyone else so leaving it as it is: Old: {order} - New: {order}""") + New: {order}""" + ) new_orders += [order] else: new_order: mango.Order = order.with_price(new_price) - self._logger.debug(f"""Order change - top of book from others is {self.adjustment_ticks} tick from {new_price}: + self._logger.debug( + f"""Order change - top of book from others is {self.adjustment_ticks} tick from {new_price}: Old: {order} - New: {new_order}""") + New: {new_order}""" + ) new_orders += [new_order] return new_orders diff --git a/mango/marketmaking/orderreconciler.py b/mango/marketmaking/orderreconciler.py index a05272e..b89e511 100644 --- a/mango/marketmaking/orderreconciler.py +++ b/mango/marketmaking/orderreconciler.py @@ -32,8 +32,15 @@ class OrderReconciler(metaclass=abc.ABCMeta): self._logger: logging.Logger = logging.getLogger(self.__class__.__name__) @abc.abstractmethod - def reconcile(self, model_state: ModelState, existing_orders: typing.Sequence[mango.Order], desired_orders: typing.Sequence[mango.Order]) -> ReconciledOrders: - raise NotImplementedError("OrderReconciler.reconcile() is not implemented on the base type.") + def reconcile( + self, + model_state: ModelState, + existing_orders: typing.Sequence[mango.Order], + desired_orders: typing.Sequence[mango.Order], + ) -> ReconciledOrders: + raise NotImplementedError( + "OrderReconciler.reconcile() is not implemented on the base type." + ) def __str__(self) -> str: return """ยซ OrderReconciler ยป""" @@ -50,7 +57,12 @@ class NullOrderReconciler(OrderReconciler): def __init__(self) -> None: super().__init__() - def reconcile(self, _: ModelState, existing_orders: typing.Sequence[mango.Order], desired_orders: typing.Sequence[mango.Order]) -> ReconciledOrders: + def reconcile( + self, + _: ModelState, + existing_orders: typing.Sequence[mango.Order], + desired_orders: typing.Sequence[mango.Order], + ) -> ReconciledOrders: outcomes: ReconciledOrders = ReconciledOrders() outcomes.to_keep = list(existing_orders) outcomes.to_ignore = list(desired_orders) @@ -68,7 +80,12 @@ class AlwaysReplaceOrderReconciler(OrderReconciler): def __init__(self) -> None: super().__init__() - def reconcile(self, _: ModelState, existing_orders: typing.Sequence[mango.Order], desired_orders: typing.Sequence[mango.Order]) -> ReconciledOrders: + def reconcile( + self, + _: ModelState, + existing_orders: typing.Sequence[mango.Order], + desired_orders: typing.Sequence[mango.Order], + ) -> ReconciledOrders: outcomes: ReconciledOrders = ReconciledOrders() outcomes.to_cancel = list(existing_orders) outcomes.to_place = list(desired_orders) diff --git a/mango/marketmaking/toleranceorderreconciler.py b/mango/marketmaking/toleranceorderreconciler.py index dc09124..9698c7e 100644 --- a/mango/marketmaking/toleranceorderreconciler.py +++ b/mango/marketmaking/toleranceorderreconciler.py @@ -50,7 +50,12 @@ class ToleranceOrderReconciler(OrderReconciler): def zero_tolerance_order_reconciler() -> "ToleranceOrderReconciler": return ToleranceOrderReconciler(Decimal(0), Decimal(0)) - def reconcile(self, _: ModelState, existing_orders: typing.Sequence[mango.Order], desired_orders: typing.Sequence[mango.Order]) -> ReconciledOrders: + def reconcile( + self, + _: ModelState, + existing_orders: typing.Sequence[mango.Order], + desired_orders: typing.Sequence[mango.Order], + ) -> ReconciledOrders: remaining_existing_orders: typing.List[mango.Order] = list(existing_orders) outcomes: ReconciledOrders = ReconciledOrders() for desired in desired_orders: @@ -67,14 +72,22 @@ class ToleranceOrderReconciler(OrderReconciler): outcomes.to_cancel = remaining_existing_orders in_count = len(existing_orders) + len(desired_orders) - out_count = len(outcomes.to_place) + len(outcomes.to_cancel) + len(outcomes.to_keep) + len(outcomes.to_ignore) + out_count = ( + len(outcomes.to_place) + + len(outcomes.to_cancel) + + len(outcomes.to_keep) + + len(outcomes.to_ignore) + ) if in_count != out_count: raise Exception( - f"Failure processing all desired orders. Count of orders in: {in_count}. Count of orders out: {out_count}.") + f"Failure processing all desired orders. Count of orders in: {in_count}. Count of orders out: {out_count}." + ) return outcomes - def find_acceptable_order(self, desired: mango.Order, existing_orders: typing.Sequence[mango.Order]) -> typing.Optional[mango.Order]: + def find_acceptable_order( + self, desired: mango.Order, existing_orders: typing.Sequence[mango.Order] + ) -> typing.Optional[mango.Order]: for existing in existing_orders: if self.is_within_tolderance(existing, desired): return existing diff --git a/mango/marketoperations.py b/mango/marketoperations.py index 905c176..123d75d 100644 --- a/mango/marketoperations.py +++ b/mango/marketoperations.py @@ -62,29 +62,38 @@ class MarketInstructionBuilder(metaclass=abc.ABCMeta): self._logger: logging.Logger = logging.getLogger(self.__class__.__name__) @abc.abstractmethod - def build_cancel_order_instructions(self, order: Order, ok_if_missing: bool = False) -> CombinableInstructions: + def build_cancel_order_instructions( + self, order: Order, ok_if_missing: bool = False + ) -> CombinableInstructions: raise NotImplementedError( - "MarketInstructionBuilder.build_cancel_order_instructions() is not implemented on the base type.") + "MarketInstructionBuilder.build_cancel_order_instructions() is not implemented on the base type." + ) @abc.abstractmethod def build_place_order_instructions(self, order: Order) -> CombinableInstructions: raise NotImplementedError( - "MarketInstructionBuilder.build_place_order_instructions() is not implemented on the base type.") + "MarketInstructionBuilder.build_place_order_instructions() is not implemented on the base type." + ) @abc.abstractmethod def build_settle_instructions(self) -> CombinableInstructions: raise NotImplementedError( - "MarketInstructionBuilder.build_settle_instructions() is not implemented on the base type.") + "MarketInstructionBuilder.build_settle_instructions() is not implemented on the base type." + ) @abc.abstractmethod - def build_crank_instructions(self, addresses: typing.Sequence[PublicKey], limit: Decimal = Decimal(32)) -> CombinableInstructions: + def build_crank_instructions( + self, addresses: typing.Sequence[PublicKey], limit: Decimal = Decimal(32) + ) -> CombinableInstructions: raise NotImplementedError( - "MarketInstructionBuilder.build_crank_instructions() is not implemented on the base type.") + "MarketInstructionBuilder.build_crank_instructions() is not implemented on the base type." + ) @abc.abstractmethod def build_redeem_instructions(self) -> CombinableInstructions: raise NotImplementedError( - "MarketInstructionBuilder.build_redeem_instructions() is not implemented on the base type.") + "MarketInstructionBuilder.build_redeem_instructions() is not implemented on the base type." + ) def __repr__(self) -> str: return f"{self}" @@ -110,36 +119,54 @@ class MarketOperations(metaclass=abc.ABCMeta): self.market: Market = market @abc.abstractmethod - def cancel_order(self, order: Order, ok_if_missing: bool = False) -> typing.Sequence[str]: - raise NotImplementedError("MarketOperations.cancel_order() is not implemented on the base type.") + def cancel_order( + self, order: Order, ok_if_missing: bool = False + ) -> typing.Sequence[str]: + raise NotImplementedError( + "MarketOperations.cancel_order() is not implemented on the base type." + ) @abc.abstractmethod def place_order(self, order: Order, crank_limit: Decimal = Decimal(5)) -> Order: - raise NotImplementedError("MarketOperations.place_order() is not implemented on the base type.") + raise NotImplementedError( + "MarketOperations.place_order() is not implemented on the base type." + ) @abc.abstractmethod def load_orderbook(self) -> OrderBook: - raise NotImplementedError("MarketOperations.load_orders() is not implemented on the base type.") + raise NotImplementedError( + "MarketOperations.load_orders() is not implemented on the base type." + ) @abc.abstractmethod def load_my_orders(self) -> typing.Sequence[Order]: - raise NotImplementedError("MarketOperations.load_my_orders() is not implemented on the base type.") + raise NotImplementedError( + "MarketOperations.load_my_orders() is not implemented on the base type." + ) @abc.abstractmethod def settle(self) -> typing.Sequence[str]: - raise NotImplementedError("MarketOperations.settle() is not implemented on the base type.") + raise NotImplementedError( + "MarketOperations.settle() is not implemented on the base type." + ) @abc.abstractmethod def crank(self, limit: Decimal = Decimal(32)) -> typing.Sequence[str]: - raise NotImplementedError("MarketOperations.crank() is not implemented on the base type.") + raise NotImplementedError( + "MarketOperations.crank() is not implemented on the base type." + ) @abc.abstractmethod def create_openorders(self) -> PublicKey: - raise NotImplementedError("MarketOperations.create_openorders() is not implemented on the base type.") + raise NotImplementedError( + "MarketOperations.create_openorders() is not implemented on the base type." + ) @abc.abstractmethod def ensure_openorders(self) -> PublicKey: - raise NotImplementedError("MarketOperations.ensure_openorders() is not implemented on the base type.") + raise NotImplementedError( + "MarketOperations.ensure_openorders() is not implemented on the base type." + ) def __repr__(self) -> str: return f"{self}" @@ -155,7 +182,9 @@ class NullMarketInstructionBuilder(MarketInstructionBuilder): super().__init__() self.symbol: str = symbol - def build_cancel_order_instructions(self, order: Order, ok_if_missing: bool = False) -> CombinableInstructions: + def build_cancel_order_instructions( + self, order: Order, ok_if_missing: bool = False + ) -> CombinableInstructions: return CombinableInstructions.empty() def build_place_order_instructions(self, order: Order) -> CombinableInstructions: @@ -164,7 +193,9 @@ class NullMarketInstructionBuilder(MarketInstructionBuilder): def build_settle_instructions(self) -> CombinableInstructions: return CombinableInstructions.empty() - def build_crank_instructions(self, addresses: typing.Sequence[PublicKey], limit: Decimal = Decimal(32)) -> CombinableInstructions: + def build_crank_instructions( + self, addresses: typing.Sequence[PublicKey], limit: Decimal = Decimal(32) + ) -> CombinableInstructions: return CombinableInstructions.empty() def build_redeem_instructions(self) -> CombinableInstructions: @@ -184,7 +215,9 @@ class NullMarketOperations(MarketOperations): super().__init__(DryRunMarket(market_name)) self.market_name: str = market_name - def cancel_order(self, order: Order, ok_if_missing: bool = False) -> typing.Sequence[str]: + def cancel_order( + self, order: Order, ok_if_missing: bool = False + ) -> typing.Sequence[str]: self._logger.info(f"[Dry Run] Not cancelling order {order}.") return [""] diff --git a/mango/metadata.py b/mango/metadata.py index b55268b..32c9af5 100644 --- a/mango/metadata.py +++ b/mango/metadata.py @@ -22,8 +22,10 @@ from .version import Version # # ๐Ÿฅญ Metadata class # -class Metadata(): - def __init__(self, data_type: typing.Any, version: Version, is_initialized: bool) -> None: +class Metadata: + def __init__( + self, data_type: typing.Any, version: Version, is_initialized: bool + ) -> None: self._logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.data_type: typing.Any = data_type self.version: Version = version diff --git a/mango/modelstate.py b/mango/modelstate.py index 7c49c09..0c6970f 100644 --- a/mango/modelstate.py +++ b/mango/modelstate.py @@ -40,7 +40,9 @@ from .watcher import Watcher class EventQueue(typing.Protocol): @property def accounts_to_crank(self) -> typing.Sequence[PublicKey]: - raise NotImplementedError("EventQueue.accounts_to_crank is not implemented on the Protocol.") + raise NotImplementedError( + "EventQueue.accounts_to_crank is not implemented on the Protocol." + ) class NullEventQueue: @@ -54,17 +56,18 @@ class NullEventQueue: # Provides simple access to the latest state of market and account data. # class ModelState: - def __init__(self, - order_owner: PublicKey, - market: Market, - group_watcher: Watcher[Group], - account_watcher: Watcher[Account], - price_watcher: Watcher[Price], - placed_orders_container_watcher: Watcher[PlacedOrdersContainer], - inventory_watcher: Watcher[Inventory], - orderbook: Watcher[OrderBook], - event_queue: Watcher[EventQueue] - ) -> None: + def __init__( + self, + order_owner: PublicKey, + market: Market, + group_watcher: Watcher[Group], + account_watcher: Watcher[Account], + price_watcher: Watcher[Price], + placed_orders_container_watcher: Watcher[PlacedOrdersContainer], + inventory_watcher: Watcher[Inventory], + orderbook: Watcher[OrderBook], + event_queue: Watcher[EventQueue], + ) -> None: self._logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.order_owner: PublicKey = order_owner self.market: Market = market @@ -72,7 +75,8 @@ class ModelState: self.account_watcher: Watcher[Account] = account_watcher self.price_watcher: Watcher[Price] = price_watcher self.placed_orders_container_watcher: Watcher[ - PlacedOrdersContainer] = placed_orders_container_watcher + PlacedOrdersContainer + ] = placed_orders_container_watcher self.inventory_watcher: Watcher[Inventory] = inventory_watcher self.orderbook_watcher: Watcher[OrderBook] = orderbook self.event_queue_watcher: Watcher[EventQueue] = event_queue diff --git a/mango/notification.py b/mango/notification.py index 1ff3782..4b39061 100644 --- a/mango/notification.py +++ b/mango/notification.py @@ -52,7 +52,9 @@ class NotificationTarget(metaclass=abc.ABCMeta): @abc.abstractmethod def send_notification(self, item: typing.Any) -> None: - raise NotImplementedError("NotificationTarget.send() is not implemented on the base type.") + raise NotImplementedError( + "NotificationTarget.send() is not implemented on the base type." + ) def __str__(self) -> str: return "ยซ NotificationTarget ยป" @@ -87,7 +89,11 @@ class TelegramNotificationTarget(NotificationTarget): self.bot_id = bot_id def send_notification(self, item: typing.Any) -> None: - payload = {"disable_notification": True, "chat_id": self.chat_id, "text": str(item)} + payload = { + "disable_notification": True, + "chat_id": self.chat_id, + "text": str(item), + } url = f"https://api.telegram.org/bot{self.bot_id}/sendMessage" headers = {"Content-Type": "application/json"} requests.post(url, json=payload, headers=headers) @@ -106,9 +112,7 @@ class DiscordNotificationTarget(NotificationTarget): self.address = address def send_notification(self, item: typing.Any) -> None: - payload = { - "content": str(item) - } + payload = {"content": str(item)} url = self.address headers = {"Content-Type": "application/json"} requests.post(url, json=payload, headers=headers) @@ -171,7 +175,15 @@ class MailjetNotificationTarget(NotificationTarget): def __init__(self, encoded_parameters: str) -> None: super().__init__() self.address = "https://api.mailjet.com/v3.1/send" - api_key, api_secret, subject, from_name, from_address, to_name, to_address = encoded_parameters.split(":") + ( + api_key, + api_secret, + subject, + from_name, + from_address, + to_name, + to_address, + ) = encoded_parameters.split(":") self.api_key: str = unquote(api_key) self.api_secret: str = unquote(api_secret) self.subject: str = unquote(subject) @@ -184,25 +196,19 @@ class MailjetNotificationTarget(NotificationTarget): payload = { "Messages": [ { - "From": { - "Email": self.from_address, - "Name": self.from_name - }, + "From": {"Email": self.from_address, "Name": self.from_name}, "Subject": self.subject, "TextPart": str(item), - "To": [ - { - "Email": self.to_address, - "Name": self.to_name - } - ] + "To": [{"Email": self.to_address, "Name": self.to_name}], } ] } url = self.address headers = {"Content-Type": "application/json"} - requests.post(url, json=payload, headers=headers, auth=(self.api_key, self.api_secret)) + requests.post( + url, json=payload, headers=headers, auth=(self.api_key, self.api_secret) + ) def __str__(self) -> str: return f"ยซ MailjetNotificationTarget To: '{self.to_name}' '{self.to_address}' with subject '{self.subject}' ยป" @@ -232,12 +238,20 @@ class CsvFileNotificationTarget(NotificationTarget): if not os.path.isfile(self.filename) or os.path.getsize(self.filename) == 0: with open(self.filename, "w") as empty_file: empty_file.write( - '"Timestamp","Liquidator Name","Group","Succeeded","Signature","Wallet","Margin Account","Token Changes"\n') + '"Timestamp","Liquidator Name","Group","Succeeded","Signature","Wallet","Margin Account","Token Changes"\n' + ) with open(self.filename, "a") as csvfile: result = "Succeeded" if event.succeeded else "Failed" - row_data = [event.timestamp, event.liquidator_name, event.group_name, result, - " ".join(event.signatures), event.wallet_address, event.account_address] + row_data = [ + event.timestamp, + event.liquidator_name, + event.group_name, + result, + " ".join(event.signatures), + event.wallet_address, + event.account_address, + ] for change in event.changes: row_data += [f"{change.value:.8f}", change.token.name] file_writer = csv.writer(csvfile, quoting=csv.QUOTE_MINIMAL) @@ -253,7 +267,11 @@ class CsvFileNotificationTarget(NotificationTarget): # `NotificationTarget` if the filter function returns `True` for the notification item. # class FilteringNotificationTarget(NotificationTarget): - def __init__(self, inner_notifier: NotificationTarget, filter_func: typing.Callable[[typing.Any], bool]) -> None: + def __init__( + self, + inner_notifier: NotificationTarget, + filter_func: typing.Callable[[typing.Any], bool], + ) -> None: super().__init__() self.inner_notifier: NotificationTarget = inner_notifier self.filter_func = filter_func @@ -300,7 +318,9 @@ class CompoundNotificationTarget(NotificationTarget): except Exception as exception: if not self.in_exception_handler: self.in_exception_handler = True - self._logger.error(f"Failed to send notification to: {target} - {exception}") + self._logger.error( + f"Failed to send notification to: {target} - {exception}" + ) finally: self.in_exception_handler = False diff --git a/mango/observables.py b/mango/observables.py index b0b5dc7..a3062cb 100644 --- a/mango/observables.py +++ b/mango/observables.py @@ -127,14 +127,16 @@ class CaptureFirstItem: # # The `TItem` type parameter is the type parameter for the generic `LatestItemObserverSubscriber`. # -TItem = typing.TypeVar('TItem') +TItem = typing.TypeVar("TItem") # # ๐Ÿฅญ LatestItemObserverSubscriber class # # This class can subscribe to an `Observable` and capture the latest item as it is observed. # -class LatestItemObserverSubscriber(rx.core.observer.observer.Observer, typing.Generic[TItem]): +class LatestItemObserverSubscriber( + rx.core.observer.observer.Observer, typing.Generic[TItem] +): def __init__(self, initial: TItem) -> None: super().__init__() self.latest: TItem = initial @@ -160,10 +162,12 @@ class LatestItemObserverSubscriber(rx.core.observer.observer.Observer, typing.Ge # component functions. # class FunctionObserver(rx.core.observer.observer.Observer): - def __init__(self, - on_next: typing.Callable[[typing.Any], None], - on_error: typing.Callable[[Exception], None] = lambda _: None, - on_completed: typing.Callable[[], None] = lambda: None) -> None: + def __init__( + self, + on_next: typing.Callable[[typing.Any], None], + on_error: typing.Callable[[Exception], None] = lambda _: None, + on_completed: typing.Callable[[], None] = lambda: None, + ) -> None: self._logger: logging.Logger = logging.getLogger(self.__class__.__name__) self._on_next = on_next self._on_error = on_error @@ -215,9 +219,17 @@ class DisposingSubject(rx.subject.subject.Subject): # take multiple seconds to complete. In that case, the latest item will be immediately # emitted and the in-between items skipped. # -def create_backpressure_skipping_observer(on_next: typing.Callable[[typing.Any], None], on_error: typing.Callable[[Exception], None] = lambda _: None, on_completed: typing.Callable[[], None] = lambda: None) -> rx.core.typing.Observer[typing.Any]: - observer = FunctionObserver(on_next=on_next, on_error=on_error, on_completed=on_completed) - return typing.cast(rx.core.typing.Observer[typing.Any], BackPressure.LATEST(observer)) +def create_backpressure_skipping_observer( + on_next: typing.Callable[[typing.Any], None], + on_error: typing.Callable[[Exception], None] = lambda _: None, + on_completed: typing.Callable[[], None] = lambda: None, +) -> rx.core.typing.Observer[typing.Any]: + observer = FunctionObserver( + on_next=on_next, on_error=on_error, on_completed=on_completed + ) + return typing.cast( + rx.core.typing.Observer[typing.Any], BackPressure.LATEST(observer) + ) # # ๐Ÿฅญ debug_print_item function @@ -237,6 +249,7 @@ def debug_print_item(title: str) -> typing.Callable[[typing.Any], typing.Any]: def _debug_print_item(item: typing.Any) -> typing.Any: output(title, item) return item + return _debug_print_item @@ -277,7 +290,9 @@ def log_subscription_error(error: Exception) -> None: # sub1.subscribe(lambda item: print(item), on_error = lambda error: print(f"Error : {error}")) # ``` # -def observable_pipeline_error_reporter(ex: Exception, _: rx.core.observable.observable.Observable) -> rx.core.observable.observable.Observable: +def observable_pipeline_error_reporter( + ex: Exception, _: rx.core.observable.observable.Observable +) -> rx.core.observable.observable.Observable: logging.error(f"Intercepted error in observable pipeline: {ex}") raise ex @@ -286,7 +301,7 @@ def observable_pipeline_error_reporter(ex: Exception, _: rx.core.observable.obse # # The `TEventDatum` type parameter is the type parameter for the generic `LatestItemObserverSubscriber`. # -TEventDatum = typing.TypeVar('TEventDatum') +TEventDatum = typing.TypeVar("TEventDatum") # # ๐Ÿฅญ EventSource class diff --git a/mango/openorders.py b/mango/openorders.py index f84abd4..0de8250 100644 --- a/mango/openorders.py +++ b/mango/openorders.py @@ -35,11 +35,21 @@ from .version import Version # # ๐Ÿฅญ OpenOrders class # class OpenOrders(AddressableAccount): - def __init__(self, account_info: AccountInfo, version: Version, program_address: PublicKey, - account_flags: AccountFlags, market: PublicKey, owner: PublicKey, - base_token_free: Decimal, base_token_total: Decimal, quote_token_free: Decimal, - quote_token_total: Decimal, placed_orders: typing.Sequence[PlacedOrder], - referrer_rebate_accrued: Decimal) -> None: + def __init__( + self, + account_info: AccountInfo, + version: Version, + program_address: PublicKey, + account_flags: AccountFlags, + market: PublicKey, + owner: PublicKey, + base_token_free: Decimal, + base_token_total: Decimal, + quote_token_free: Decimal, + quote_token_total: Decimal, + placed_orders: typing.Sequence[PlacedOrder], + referrer_rebate_accrued: Decimal, + ) -> None: super().__init__(account_info) self.version: Version = version self.program_address: PublicKey = program_address @@ -66,78 +76,142 @@ class OpenOrders(AddressableAccount): return PySerumOpenOrdersAccount.from_bytes(self.address, self.account_info.data) @staticmethod - def from_layout(layout: typing.Any, account_info: AccountInfo, - base_decimals: Decimal, quote_decimals: Decimal) -> "OpenOrders": + def from_layout( + layout: typing.Any, + account_info: AccountInfo, + base_decimals: Decimal, + quote_decimals: Decimal, + ) -> "OpenOrders": account_flags = AccountFlags.from_layout(layout.account_flags) program_address = account_info.owner - base_divisor = 10 ** base_decimals - quote_divisor = 10 ** quote_decimals + base_divisor = 10**base_decimals + quote_divisor = 10**quote_decimals base_token_free: Decimal = layout.base_token_free / base_divisor base_token_total: Decimal = layout.base_token_total / base_divisor quote_token_free: Decimal = layout.quote_token_free / quote_divisor quote_token_total: Decimal = layout.quote_token_total / quote_divisor - referrer_rebate_accrued: Decimal = layout.referrer_rebate_accrued / quote_divisor + referrer_rebate_accrued: Decimal = ( + layout.referrer_rebate_accrued / quote_divisor + ) placed_orders: typing.Sequence[PlacedOrder] = [] if account_flags.initialized: placed_orders = PlacedOrder.build_from_open_orders_data( - layout.free_slot_bits, layout.is_bid_bits, layout.orders, layout.client_ids) - return OpenOrders(account_info, Version.UNSPECIFIED, program_address, account_flags, layout.market, - layout.owner, base_token_free, base_token_total, quote_token_free, - quote_token_total, placed_orders, referrer_rebate_accrued) + layout.free_slot_bits, + layout.is_bid_bits, + layout.orders, + layout.client_ids, + ) + return OpenOrders( + account_info, + Version.UNSPECIFIED, + program_address, + account_flags, + layout.market, + layout.owner, + base_token_free, + base_token_total, + quote_token_free, + quote_token_total, + placed_orders, + referrer_rebate_accrued, + ) @staticmethod - def parse(account_info: AccountInfo, base_decimals: Decimal, quote_decimals: Decimal) -> "OpenOrders": + def parse( + account_info: AccountInfo, base_decimals: Decimal, quote_decimals: Decimal + ) -> "OpenOrders": data = account_info.data if len(data) != layouts.OPEN_ORDERS.sizeof(): - raise Exception(f"Data length ({len(data)}) does not match expected size ({layouts.OPEN_ORDERS.sizeof()})") + raise Exception( + f"Data length ({len(data)}) does not match expected size ({layouts.OPEN_ORDERS.sizeof()})" + ) layout = layouts.OPEN_ORDERS.parse(data) - return OpenOrders.from_layout(layout, account_info, base_decimals, quote_decimals) + return OpenOrders.from_layout( + layout, account_info, base_decimals, quote_decimals + ) @staticmethod - def load_raw_open_orders_account_infos(context: Context, group: Group) -> typing.Dict[str, AccountInfo]: + def load_raw_open_orders_account_infos( + context: Context, group: Group + ) -> typing.Dict[str, AccountInfo]: filters = [ MemcmpOpts( offset=layouts.ACCOUNT_FLAGS.sizeof() + 37, - bytes=encode_key(group.signer_key) + bytes=encode_key(group.signer_key), ) ] results = context.client.get_program_accounts( - group.serum_program_address, data_size=layouts.OPEN_ORDERS.sizeof(), memcmp_opts=filters) - account_infos = list(map(lambda pair: AccountInfo._from_response_values(pair[0], pair[1]), [ - (result["account"], PublicKey(result["pubkey"])) for result in results])) - account_infos_by_address = {key: value for key, value in [ - (str(account_info.address), account_info) for account_info in account_infos]} + group.serum_program_address, + data_size=layouts.OPEN_ORDERS.sizeof(), + memcmp_opts=filters, + ) + account_infos = list( + map( + lambda pair: AccountInfo._from_response_values(pair[0], pair[1]), + [ + (result["account"], PublicKey(result["pubkey"])) + for result in results + ], + ) + ) + account_infos_by_address = { + key: value + for key, value in [ + (str(account_info.address), account_info) + for account_info in account_infos + ] + } return account_infos_by_address @staticmethod - def load(context: Context, address: PublicKey, base_decimals: Decimal, quote_decimals: Decimal) -> "OpenOrders": + def load( + context: Context, + address: PublicKey, + base_decimals: Decimal, + quote_decimals: Decimal, + ) -> "OpenOrders": open_orders_account = AccountInfo.load(context, address) if open_orders_account is None: raise Exception(f"OpenOrders account not found at address '{address}'") return OpenOrders.parse(open_orders_account, base_decimals, quote_decimals) @staticmethod - def load_for_market_and_owner(context: Context, market: PublicKey, owner: PublicKey, program_address: PublicKey, base_decimals: Decimal, quote_decimals: Decimal) -> typing.Sequence["OpenOrders"]: + def load_for_market_and_owner( + context: Context, + market: PublicKey, + owner: PublicKey, + program_address: PublicKey, + base_decimals: Decimal, + quote_decimals: Decimal, + ) -> typing.Sequence["OpenOrders"]: filters = [ MemcmpOpts( - offset=layouts.ACCOUNT_FLAGS.sizeof() + 5, - bytes=encode_key(market) + offset=layouts.ACCOUNT_FLAGS.sizeof() + 5, bytes=encode_key(market) ), MemcmpOpts( - offset=layouts.ACCOUNT_FLAGS.sizeof() + 37, - bytes=encode_key(owner) - ) + offset=layouts.ACCOUNT_FLAGS.sizeof() + 37, bytes=encode_key(owner) + ), ] results = context.client.get_program_accounts( - program_address, data_size=layouts.OPEN_ORDERS.sizeof(), memcmp_opts=filters) - accounts = map(lambda result: AccountInfo._from_response_values( - result["account"], PublicKey(result["pubkey"])), results) - return list(map(lambda acc: OpenOrders.parse(acc, base_decimals, quote_decimals), accounts)) + program_address, data_size=layouts.OPEN_ORDERS.sizeof(), memcmp_opts=filters + ) + accounts = map( + lambda result: AccountInfo._from_response_values( + result["account"], PublicKey(result["pubkey"]) + ), + results, + ) + return list( + map( + lambda acc: OpenOrders.parse(acc, base_decimals, quote_decimals), + accounts, + ) + ) def __str__(self) -> str: placed_orders = "\n ".join(map(str, self.placed_orders)) or "None" diff --git a/mango/oracle.py b/mango/oracle.py index 11e11d4..fe102e0 100644 --- a/mango/oracle.py +++ b/mango/oracle.py @@ -50,8 +50,14 @@ class SupportedOracleFeature(enum.Flag): # This class describes an oracle and can be used to tell apart `Prices` from different `Oracle`s # apart. # -class OracleSource(): - def __init__(self, provider_name: str, source_name: str, supports: SupportedOracleFeature, market: Market) -> None: +class OracleSource: + def __init__( + self, + provider_name: str, + source_name: str, + supports: SupportedOracleFeature, + market: Market, + ) -> None: self.provider_name = provider_name self.source_name = source_name self.supports: SupportedOracleFeature = supports @@ -68,8 +74,17 @@ class OracleSource(): # # This class contains all relevant info for a price. # -class Price(): - def __init__(self, source: OracleSource, timestamp: datetime, market: Market, top_bid: Decimal, mid_price: Decimal, top_ask: Decimal, confidence: Decimal) -> None: +class Price: + def __init__( + self, + source: OracleSource, + timestamp: datetime, + market: Market, + top_bid: Decimal, + mid_price: Decimal, + top_ask: Decimal, + confidence: Decimal, + ) -> None: self.source: OracleSource = source self.timestamp: datetime = timestamp self.market: Market = market @@ -108,11 +123,17 @@ class Oracle(metaclass=abc.ABCMeta): @abc.abstractmethod def fetch_price(self, context: Context) -> Price: - raise NotImplementedError("Oracle.fetch_price() is not implemented on the base type.") + raise NotImplementedError( + "Oracle.fetch_price() is not implemented on the base type." + ) @abc.abstractmethod - def to_streaming_observable(self, context: Context) -> rx.core.typing.Observable[Price]: - raise NotImplementedError("Oracle.fetch_price() is not implemented on the base type.") + def to_streaming_observable( + self, context: Context + ) -> rx.core.typing.Observable[Price]: + raise NotImplementedError( + "Oracle.fetch_price() is not implemented on the base type." + ) def __str__(self) -> str: return f"ยซ Oracle {self.name} [{self.market.symbol}] ยป" @@ -130,12 +151,18 @@ class OracleProvider(metaclass=abc.ABCMeta): self.name = name @abc.abstractmethod - def oracle_for_market(self, context: Context, market: Market) -> typing.Optional[Oracle]: - raise NotImplementedError("OracleProvider.create_oracle_for_market() is not implemented on the base type.") + def oracle_for_market( + self, context: Context, market: Market + ) -> typing.Optional[Oracle]: + raise NotImplementedError( + "OracleProvider.create_oracle_for_market() is not implemented on the base type." + ) @abc.abstractmethod def all_available_symbols(self, context: Context) -> typing.Sequence[str]: - raise NotImplementedError("OracleProvider.all_available_symbols() is not implemented on the base type.") + raise NotImplementedError( + "OracleProvider.all_available_symbols() is not implemented on the base type." + ) def __str__(self) -> str: return f"ยซ OracleProvider {self.name} ยป" diff --git a/mango/oraclefactory.py b/mango/oraclefactory.py index adf3f29..e12316a 100644 --- a/mango/oraclefactory.py +++ b/mango/oraclefactory.py @@ -35,7 +35,9 @@ def create_oracle_provider(context: Context, provider_name: str) -> OracleProvid elif proper_provider_name == "PYTH": return pythnetwork.PythOracleProvider(context) elif proper_provider_name == "PYTH-MAINNET": - mainnet_beta_pyth_context: Context = ContextBuilder.forced_to_mainnet_beta(context) + mainnet_beta_pyth_context: Context = ContextBuilder.forced_to_mainnet_beta( + context + ) return pythnetwork.PythOracleProvider(mainnet_beta_pyth_context) elif proper_provider_name == "PYTH-DEVNET": devnet_pyth_context: Context = ContextBuilder.forced_to_devnet(context) diff --git a/mango/oracles/ftx/ftx.py b/mango/oracles/ftx/ftx.py index b488a2d..a82e0e1 100644 --- a/mango/oracles/ftx/ftx.py +++ b/mango/oracles/ftx/ftx.py @@ -27,7 +27,13 @@ from rx.subject.subject import Subject from ...context import Context from ...market import Market from ...observables import DisposePropagator, DisposeWrapper -from ...oracle import Oracle, OracleProvider, OracleSource, Price, SupportedOracleFeature +from ...oracle import ( + Oracle, + OracleProvider, + OracleSource, + Price, + SupportedOracleFeature, +) from ...reconnectingwebsocket import ReconnectingWebsocket @@ -60,7 +66,9 @@ class FtxOracle(Oracle): super().__init__(name, market) self.market: Market = market self.ftx_symbol: str = ftx_symbol - features: SupportedOracleFeature = SupportedOracleFeature.MID_PRICE | SupportedOracleFeature.TOP_BID_AND_OFFER + features: SupportedOracleFeature = ( + SupportedOracleFeature.MID_PRICE | SupportedOracleFeature.TOP_BID_AND_OFFER + ) self.source: OracleSource = OracleSource("FTX", name, features, market) def fetch_price(self, context: Context) -> Price: @@ -69,7 +77,15 @@ class FtxOracle(Oracle): ask = Decimal(result["ask"]) price = Decimal(result["price"]) - return Price(self.source, datetime.now(), self.market, bid, price, ask, FtxOracleConfidence) + return Price( + self.source, + datetime.now(), + self.market, + bid, + price, + ask, + FtxOracleConfidence, + ) def to_streaming_observable(self, _: Context) -> rx.core.typing.Observable[Price]: subject = Subject() @@ -81,16 +97,29 @@ class FtxOracle(Oracle): mid = (bid + ask) / Decimal(2) time = data["data"]["time"] timestamp = datetime.fromtimestamp(time) - price = Price(self.source, timestamp, self.market, bid, mid, ask, FtxOracleConfidence) + price = Price( + self.source, + timestamp, + self.market, + bid, + mid, + ask, + FtxOracleConfidence, + ) subject.on_next(price) def on_open(sock: websocket.WebSocketApp) -> None: - sock.send(f"""{{"op": "subscribe", "channel": "ticker", "market": "{self.ftx_symbol}"}}""") + sock.send( + f"""{{"op": "subscribe", "channel": "ticker", "market": "{self.ftx_symbol}"}}""" + ) ws: ReconnectingWebsocket = ReconnectingWebsocket("wss://ftx.com/ws/", on_open) ws.item.subscribe(on_next=_on_item) # type: ignore[call-arg] - def subscribe(observer: rx.core.typing.Observer[Price], scheduler_: typing.Optional[rx.core.typing.Scheduler] = None) -> rx.core.typing.Disposable: + def subscribe( + observer: rx.core.typing.Observer[Price], + scheduler_: typing.Optional[rx.core.typing.Scheduler] = None, + ) -> rx.core.typing.Disposable: subject.subscribe(observer, scheduler=scheduler_) # type: ignore disposable = DisposePropagator() @@ -111,11 +140,14 @@ class FtxOracle(Oracle): # Implements the `OracleProvider` abstract base class specialised to the Ftx Network. # + class FtxOracleProvider(OracleProvider): def __init__(self) -> None: super().__init__("Ftx Oracle Factory") - def oracle_for_market(self, context: Context, market: Market) -> typing.Optional[Oracle]: + def oracle_for_market( + self, context: Context, market: Market + ) -> typing.Optional[Oracle]: symbol = self._market_symbol_to_ftx_symbol(market.symbol) return FtxOracle(market, symbol) diff --git a/mango/oracles/market/market.py b/mango/oracles/market/market.py index a4e3c81..90296cb 100644 --- a/mango/oracles/market/market.py +++ b/mango/oracles/market/market.py @@ -26,7 +26,13 @@ from ...ensuremarketloaded import ensure_market_loaded from ...loadedmarket import LoadedMarket from ...market import Market from ...observables import observable_pipeline_error_reporter -from ...oracle import Oracle, OracleProvider, OracleSource, Price, SupportedOracleFeature +from ...oracle import ( + Oracle, + OracleProvider, + OracleSource, + Price, + SupportedOracleFeature, +) from ...orders import OrderBook @@ -61,20 +67,36 @@ class MarketOracle(Oracle): def fetch_price(self, context: Context) -> Price: orderbook: OrderBook = self.loaded_market.fetch_orderbook(context) if orderbook.top_bid is None: - raise Exception(f"[{self.source}] Cannot determine complete price data - no top bid") + raise Exception( + f"[{self.source}] Cannot determine complete price data - no top bid" + ) top_bid = orderbook.top_bid.price if orderbook.top_ask is None: - raise Exception(f"[{self.source}] Cannot determine complete price data - no top bid") + raise Exception( + f"[{self.source}] Cannot determine complete price data - no top bid" + ) top_ask = orderbook.top_ask.price if orderbook.mid_price is None: - raise Exception(f"[{self.source}] Cannot determine complete price data - no mid price") + raise Exception( + f"[{self.source}] Cannot determine complete price data - no mid price" + ) mid_price = orderbook.mid_price - return Price(self.source, datetime.now(), self.market, top_bid, mid_price, top_ask, MarketOracleConfidence) + return Price( + self.source, + datetime.now(), + self.market, + top_bid, + mid_price, + top_ask, + MarketOracleConfidence, + ) - def to_streaming_observable(self, context: Context) -> rx.core.typing.Observable[Price]: + def to_streaming_observable( + self, context: Context + ) -> rx.core.typing.Observable[Price]: prices = rx.interval(1).pipe( ops.observe_on(context.create_thread_pool_scheduler()), ops.start_with(-1), @@ -93,7 +115,9 @@ class MarketOracleProvider(OracleProvider): def __init__(self) -> None: super().__init__("Market Oracle Factory") - def oracle_for_market(self, context: Context, market: Market) -> typing.Optional[Oracle]: + def oracle_for_market( + self, context: Context, market: Market + ) -> typing.Optional[Oracle]: loaded_market: LoadedMarket = ensure_market_loaded(context, market) return MarketOracle(loaded_market) diff --git a/mango/oracles/pythnetwork/layouts.py b/mango/oracles/pythnetwork/layouts.py index 26f17ef..4a426f6 100644 --- a/mango/oracles/pythnetwork/layouts.py +++ b/mango/oracles/pythnetwork/layouts.py @@ -31,7 +31,7 @@ from ...layouts.layouts import DecimalAdapter, PublicKeyAdapter # Pyth defines some constants. # -MAGIC = 0xa1b2c3d4 +MAGIC = 0xA1B2C3D4 VERSION_2 = 2 VERSION = VERSION_2 MAP_TABLE_SIZE = 640 @@ -57,7 +57,9 @@ PYTH_MAINNET_MAPPING_ROOT = PublicKey("AHtgzX45WTKfkPG53L6WYhGEXwQkN1BVknET3sVsL # Price # } -ACCOUNT_TYPE = construct.Enum(construct.Int32ul, Unknown=0, Mapping=1, Product=2, Price=3) +ACCOUNT_TYPE = construct.Enum( + construct.Int32ul, Unknown=0, Mapping=1, Product=2, Price=3 +) # # ๐Ÿฅญ PythStringDictionaryAdapter @@ -66,16 +68,27 @@ ACCOUNT_TYPE = construct.Enum(construct.Int32ul, Unknown=0, Mapping=1, Product=2 # specify the string length as an int followed by that number of UTF-8 characters. # if typing.TYPE_CHECKING: - class StringDictionaryAdapter(construct.Adapter[typing.Any, str, typing.Any, typing.Any]): + + class StringDictionaryAdapter( + construct.Adapter[typing.Any, str, typing.Any, typing.Any] + ): def __init__(self) -> None: pass + else: + class StringDictionaryAdapter(construct.Adapter): def __init__(self) -> None: - super().__init__(construct.RepeatUntil(lambda contents, lst, ctx: contents == "", - construct.PascalString(construct.VarInt, "utf8"))) + super().__init__( + construct.RepeatUntil( + lambda contents, lst, ctx: contents == "", + construct.PascalString(construct.VarInt, "utf8"), + ) + ) - def _decode(self, obj: typing.Sequence[str], context: typing.Any, path: typing.Any) -> typing.Dict[str, str]: + def _decode( + self, obj: typing.Sequence[str], context: typing.Any, path: typing.Any + ) -> typing.Dict[str, str]: result: typing.Dict[str, str] = {} for counter in range(int(len(obj) / 2)): index: int = counter * 2 @@ -84,7 +97,9 @@ else: result[key] = value return result - def _encode(self, obj: typing.Any, context: typing.Any, path: typing.Any) -> str: + def _encode( + self, obj: typing.Any, context: typing.Any, path: typing.Any + ) -> str: # Can only encode string values. return str(obj) @@ -114,7 +129,7 @@ MAPPING = construct.Struct( "num" / DecimalAdapter(4), "unused" / DecimalAdapter(4), "next" / PublicKeyAdapter(), - "products" / construct.Array(MAP_TABLE_SIZE, PublicKeyAdapter()) + "products" / construct.Array(MAP_TABLE_SIZE, PublicKeyAdapter()), ) @@ -140,7 +155,7 @@ PRODUCT = construct.Struct( "atype" / ACCOUNT_TYPE, "size" / DecimalAdapter(4), "px_acc" / PublicKeyAdapter(), - "attr" / StringDictionaryAdapter() + "attr" / StringDictionaryAdapter(), ) @@ -161,9 +176,10 @@ PRODUCT = construct.Struct( PRICE_INFO = construct.Struct( "price" / DecimalAdapter(), "conf" / DecimalAdapter(), - "status" / construct.Enum(construct.Int32ul, Unknown=0, Trading=1, Halted=2, Auction=3), + "status" + / construct.Enum(construct.Int32ul, Unknown=0, Trading=1, Halted=2, Auction=3), "corp_act" / construct.Enum(construct.Int32ul, NoCorpAct=0), - "pub_slot" / DecimalAdapter() + "pub_slot" / DecimalAdapter(), ) @@ -180,9 +196,7 @@ PRICE_INFO = construct.Struct( # latest : PriceInfo // latest contributing price (not in agg.) # } PRICE_COMP = construct.Struct( - "publisher" / PublicKeyAdapter(), - "agg" / PRICE_INFO, - "latest" / PRICE_INFO + "publisher" / PublicKeyAdapter(), "agg" / PRICE_INFO, "latest" / PRICE_INFO ) @@ -227,5 +241,5 @@ PRICE = construct.Struct( "next" / PublicKeyAdapter(), "agg_pub" / PublicKeyAdapter(), "agg" / PRICE_INFO, - "comp" / construct.Array(32, PRICE_COMP) + "comp" / construct.Array(32, PRICE_COMP), ) diff --git a/mango/oracles/pythnetwork/pythnetwork.py b/mango/oracles/pythnetwork/pythnetwork.py index b317b46..64cf9a7 100644 --- a/mango/oracles/pythnetwork/pythnetwork.py +++ b/mango/oracles/pythnetwork/pythnetwork.py @@ -27,9 +27,22 @@ from ...accountinfo import AccountInfo from ...context import Context from ...market import Market from ...observables import observable_pipeline_error_reporter -from ...oracle import Oracle, OracleProvider, OracleSource, Price, SupportedOracleFeature +from ...oracle import ( + Oracle, + OracleProvider, + OracleSource, + Price, + SupportedOracleFeature, +) -from .layouts import MAGIC, MAPPING, PRICE, PRODUCT, PYTH_DEVNET_MAPPING_ROOT, PYTH_MAINNET_MAPPING_ROOT +from .layouts import ( + MAGIC, + MAPPING, + PRICE, + PRODUCT, + PYTH_DEVNET_MAPPING_ROOT, + PYTH_MAINNET_MAPPING_ROOT, +) # # ๐Ÿฅญ Pyth @@ -59,6 +72,7 @@ from .layouts import MAGIC, MAPPING, PRICE, PRODUCT, PYTH_DEVNET_MAPPING_ROOT, P # Implements the `Oracle` abstract base class specialised to the Pyth Network. # + class PythOracle(Oracle): def __init__(self, context: Context, market: Market, product_data: typing.Any): name = f"Pyth Oracle for {market.symbol}" @@ -67,30 +81,41 @@ class PythOracle(Oracle): self.market: Market = market self.product_data: typing.Any = product_data self.address: PublicKey = product_data.address - features: SupportedOracleFeature = SupportedOracleFeature.MID_PRICE | SupportedOracleFeature.CONFIDENCE + features: SupportedOracleFeature = ( + SupportedOracleFeature.MID_PRICE | SupportedOracleFeature.CONFIDENCE + ) self.source: OracleSource = OracleSource("Pyth", name, features, market) def fetch_price(self, _: Context) -> Price: price_account_info = AccountInfo.load(self.context, self.product_data.px_acc) if price_account_info is None: - raise Exception(f"[{self.context.name}] Price account {self.product_data.px_acc} not found.") + raise Exception( + f"[{self.context.name}] Price account {self.product_data.px_acc} not found." + ) if len(price_account_info.data) != PRICE.sizeof(): raise Exception( - f"[{self.context.name}] Price account data has incorrect size. Expected: {PRICE.sizeof()}, got {len(price_account_info.data)}.") + f"[{self.context.name}] Price account data has incorrect size. Expected: {PRICE.sizeof()}, got {len(price_account_info.data)}." + ) price_data = PRICE.parse(price_account_info.data) if price_data.magic != MAGIC: - raise Exception(f"[{self.context.name}] Price account {price_account_info.address} is not a Pyth account.") + raise Exception( + f"[{self.context.name}] Price account {price_account_info.address} is not a Pyth account." + ) factor = Decimal(10) ** price_data.expo price = price_data.agg.price * factor confidence = price_data.agg.conf * factor # Pyth has no notion of bids, asks, or spreads so just provide the single price. - return Price(self.source, datetime.now(), self.market, price, price, price, confidence) + return Price( + self.source, datetime.now(), self.market, price, price, price, confidence + ) - def to_streaming_observable(self, context: Context) -> rx.core.typing.Observable[Price]: + def to_streaming_observable( + self, context: Context + ) -> rx.core.typing.Observable[Price]: prices = rx.interval(1).pipe( ops.observe_on(context.create_thread_pool_scheduler()), ops.start_with(-1), @@ -109,9 +134,14 @@ class PythOracle(Oracle): # constructor and uses that to access the data. It ignores the context passed as a parameter to its methods. # This allows the context-fudging to only happen on construction. + class PythOracleProvider(OracleProvider): def __init__(self, context: Context) -> None: - self.address: PublicKey = PYTH_MAINNET_MAPPING_ROOT if context.client.cluster_name == "mainnet" else PYTH_DEVNET_MAPPING_ROOT + self.address: PublicKey = ( + PYTH_MAINNET_MAPPING_ROOT + if context.client.cluster_name == "mainnet" + else PYTH_DEVNET_MAPPING_ROOT + ) self.__symbol_prefix = "Crypto." super().__init__(f"Pyth Oracle Factory [{self.address}]") @@ -149,29 +179,39 @@ class PythOracleProvider(OracleProvider): def _load_pyth_mapping(self, context: Context, address: PublicKey) -> typing.Any: account_info = AccountInfo.load(context, address) if account_info is None: - raise Exception(f"[{context.name}] Pyth mapping account {address} not found.") + raise Exception( + f"[{context.name}] Pyth mapping account {address} not found." + ) if len(account_info.data) != MAPPING.sizeof(): raise Exception( - f"Mapping account data has incorrect size. Expected: {MAPPING.sizeof()}, got {len(account_info.data)}.") + f"Mapping account data has incorrect size. Expected: {MAPPING.sizeof()}, got {len(account_info.data)}." + ) mapping: typing.Any = MAPPING.parse(account_info.data) mapping.address = account_info.address if mapping.magic != MAGIC: - raise Exception(f"[{context.name}] Mapping account {account_info.address} is not a Pyth account.") + raise Exception( + f"[{context.name}] Mapping account {account_info.address} is not a Pyth account." + ) return mapping - def _fetch_all_pyth_products(self, context: Context, address: PublicKey) -> typing.Sequence[typing.Any]: + def _fetch_all_pyth_products( + self, context: Context, address: PublicKey + ) -> typing.Sequence[typing.Any]: mapping = self._load_pyth_mapping(context, address) - all_product_addresses = mapping.products[0:int(mapping.num)] - product_account_infos = AccountInfo.load_multiple(context, all_product_addresses) + all_product_addresses = mapping.products[0 : int(mapping.num)] + product_account_infos = AccountInfo.load_multiple( + context, all_product_addresses + ) products: typing.List[typing.Any] = [] for product_account_info in product_account_infos: product: typing.Any = PRODUCT.parse(product_account_info.data) product.address = product_account_info.address if product.magic != MAGIC: raise Exception( - f"[{context.name}] Product account {product_account_info.address} is not a Pyth account.") + f"[{context.name}] Product account {product_account_info.address} is not a Pyth account." + ) products += [product] return products diff --git a/mango/oracles/stub/stub.py b/mango/oracles/stub/stub.py index 837e48e..b49c485 100644 --- a/mango/oracles/stub/stub.py +++ b/mango/oracles/stub/stub.py @@ -27,7 +27,13 @@ from ...context import Context from ...ensuremarketloaded import ensure_market_loaded from ...market import Market from ...observables import observable_pipeline_error_reporter -from ...oracle import Oracle, OracleProvider, OracleSource, Price, SupportedOracleFeature +from ...oracle import ( + Oracle, + OracleProvider, + OracleSource, + Price, + SupportedOracleFeature, +) from ...perpmarket import PerpMarket from ...spotmarket import SpotMarket @@ -65,7 +71,9 @@ class StubOracle(Oracle): cache: Cache = Cache.load(context, self.cache_address) raw_price = cache.price_cache[self.index] if raw_price is None: - raise Exception(f"Stub Oracle does not contain a price for market {self.symbol} at index {self.index}.") + raise Exception( + f"Stub Oracle does not contain a price for market {self.symbol} at index {self.index}." + ) # Should convert raw_price to actual price. # Discord on stub price from lagzda: # https://discord.com/channels/791995070613159966/853370356244152360/871871877382033478 @@ -73,9 +81,19 @@ class StubOracle(Oracle): # base I did the incorrect change. Instead of adjusting RAY etc stub oracles prices from 2 to # 2_000_000, I should've adjusted the Pyth oracles prices which soon will be deployed. That # will give you the consistent results, but you'll need to adjust your code" - return Price(self.source, datetime.now(), self.market, raw_price.price, raw_price.price, raw_price.price, StubOracleConfidence) + return Price( + self.source, + datetime.now(), + self.market, + raw_price.price, + raw_price.price, + raw_price.price, + StubOracleConfidence, + ) - def to_streaming_observable(self, context: Context) -> rx.core.typing.Observable[Price]: + def to_streaming_observable( + self, context: Context + ) -> rx.core.typing.Observable[Price]: prices = rx.interval(1).pipe( ops.observe_on(context.create_thread_pool_scheduler()), ops.start_with(-1), @@ -91,17 +109,24 @@ class StubOracle(Oracle): # Implements the `OracleProvider` abstract base class specialised to the Serum Network. # + class StubOracleProvider(OracleProvider): def __init__(self) -> None: super().__init__("Stub Oracle Factory") - def oracle_for_market(self, context: Context, market: Market) -> typing.Optional[Oracle]: + def oracle_for_market( + self, context: Context, market: Market + ) -> typing.Optional[Oracle]: loaded_market: Market = ensure_market_loaded(context, market) if isinstance(loaded_market, SpotMarket): - spot_index: int = loaded_market.group.slot_by_spot_market_address(loaded_market.address).index + spot_index: int = loaded_market.group.slot_by_spot_market_address( + loaded_market.address + ).index return StubOracle(loaded_market, spot_index, loaded_market.group.cache) elif isinstance(loaded_market, PerpMarket): - perp_index: int = loaded_market.group.slot_by_perp_market_address(loaded_market.address).index + perp_index: int = loaded_market.group.slot_by_perp_market_address( + loaded_market.address + ).index return StubOracle(loaded_market, perp_index, loaded_market.group.cache) return None diff --git a/mango/orderbookside.py b/mango/orderbookside.py index c934ce8..ce4f55b 100644 --- a/mango/orderbookside.py +++ b/mango/orderbookside.py @@ -50,10 +50,19 @@ class OrderBookSideType(enum.Enum): # `PerpOrderBookSide` holds orders for one side of a market. # class PerpOrderBookSide(AddressableAccount): - def __init__(self, account_info: AccountInfo, version: Version, - meta_data: Metadata, perp_market_details: PerpMarketDetails, bump_index: Decimal, - free_list_len: Decimal, free_list_head: Decimal, root_node: Decimal, - leaf_count: Decimal, nodes: typing.Any) -> None: + def __init__( + self, + account_info: AccountInfo, + version: Version, + meta_data: Metadata, + perp_market_details: PerpMarketDetails, + bump_index: Decimal, + free_list_len: Decimal, + free_list_head: Decimal, + root_node: Decimal, + leaf_count: Decimal, + nodes: typing.Any, + ) -> None: super().__init__(account_info) self.version: Version = version @@ -67,7 +76,12 @@ class PerpOrderBookSide(AddressableAccount): self.nodes: typing.Any = nodes @staticmethod - def from_layout(layout: typing.Any, account_info: AccountInfo, version: Version, perp_market_details: PerpMarketDetails) -> "PerpOrderBookSide": + def from_layout( + layout: typing.Any, + account_info: AccountInfo, + version: Version, + perp_market_details: PerpMarketDetails, + ) -> "PerpOrderBookSide": meta_data = Metadata.from_layout(layout.meta_data) bump_index: Decimal = layout.bump_index free_list_len: Decimal = layout.free_list_len @@ -76,23 +90,43 @@ class PerpOrderBookSide(AddressableAccount): leaf_count: Decimal = layout.leaf_count nodes: typing.Any = layout.nodes - return PerpOrderBookSide(account_info, version, meta_data, perp_market_details, bump_index, free_list_len, free_list_head, root_node, leaf_count, nodes) + return PerpOrderBookSide( + account_info, + version, + meta_data, + perp_market_details, + bump_index, + free_list_len, + free_list_head, + root_node, + leaf_count, + nodes, + ) @staticmethod - def parse(account_info: AccountInfo, perp_market_details: PerpMarketDetails) -> "PerpOrderBookSide": + def parse( + account_info: AccountInfo, perp_market_details: PerpMarketDetails + ) -> "PerpOrderBookSide": data = account_info.data if len(data) != layouts.ORDERBOOK_SIDE.sizeof(): raise Exception( - f"PerpOrderBookSide data length ({len(data)}) does not match expected size ({layouts.ORDERBOOK_SIDE.sizeof()})") + f"PerpOrderBookSide data length ({len(data)}) does not match expected size ({layouts.ORDERBOOK_SIDE.sizeof()})" + ) layout = layouts.ORDERBOOK_SIDE.parse(data) - return PerpOrderBookSide.from_layout(layout, account_info, Version.V1, perp_market_details) + return PerpOrderBookSide.from_layout( + layout, account_info, Version.V1, perp_market_details + ) @staticmethod - def load(context: Context, address: PublicKey, perp_market_details: PerpMarketDetails) -> "PerpOrderBookSide": + def load( + context: Context, address: PublicKey, perp_market_details: PerpMarketDetails + ) -> "PerpOrderBookSide": account_info = AccountInfo.load(context, address) if account_info is None: - raise Exception(f"PerpOrderBookSide account not found at address '{address}'") + raise Exception( + f"PerpOrderBookSide account not found at address '{address}'" + ) return PerpOrderBookSide.parse(account_info, perp_market_details) def orders(self) -> typing.Sequence[Order]: @@ -113,23 +147,33 @@ class PerpOrderBookSide(AddressableAccount): price = node.key["price"] quantity = node.quantity - decimals_differential = self.perp_market_details.base_instrument.decimals - \ - self.perp_market_details.quote_token.token.decimals + decimals_differential = ( + self.perp_market_details.base_instrument.decimals + - self.perp_market_details.quote_token.token.decimals + ) native_to_ui = Decimal(10) ** decimals_differential quote_lot_size = self.perp_market_details.quote_lot_size base_lot_size = self.perp_market_details.base_lot_size actual_price = price * (quote_lot_size / base_lot_size) * native_to_ui - base_factor = Decimal(10) ** self.perp_market_details.base_instrument.decimals - actual_quantity = (quantity * self.perp_market_details.base_lot_size) / base_factor + base_factor = ( + Decimal(10) ** self.perp_market_details.base_instrument.decimals + ) + actual_quantity = ( + quantity * self.perp_market_details.base_lot_size + ) / base_factor - orders += [Order(int(node.key["order_id"]), - node.client_order_id, - node.owner, - order_side, - actual_price, - actual_quantity, - OrderType.UNKNOWN)] + orders += [ + Order( + int(node.key["order_id"]), + node.client_order_id, + node.owner, + order_side, + actual_price, + actual_quantity, + OrderType.UNKNOWN, + ) + ] elif node.type_name == "inner": if order_side == Side.BUY: stack = [*stack, node.children[0], node.children[1]] @@ -138,7 +182,9 @@ class PerpOrderBookSide(AddressableAccount): return orders def __str__(self) -> str: - nodes = "\n ".join([str(node).replace("\n", "\n ") for node in self.orders()]) + nodes = "\n ".join( + [str(node).replace("\n", "\n ") for node in self.orders()] + ) return f"""ยซ PerpOrderBookSide {self.version} [{self.address}] {self.meta_data} Perp Market: {self.perp_market_details} diff --git a/mango/orders.py b/mango/orders.py index 4095dfb..d19901d 100644 --- a/mango/orders.py +++ b/mango/orders.py @@ -155,58 +155,133 @@ class Order: def read_sequence_number(id: int) -> Decimal: id_bytes = id.to_bytes(16, byteorder="little", signed=False) low_order = id_bytes[:8] - return Decimal(int.from_bytes(low_order, 'little', signed=False)) + return Decimal(int.from_bytes(low_order, "little", signed=False)) @staticmethod def read_price(id: int) -> Decimal: id_bytes = id.to_bytes(16, byteorder="little", signed=False) high_order = id_bytes[8:] - return Decimal(int.from_bytes(high_order, 'little', signed=False)) + return Decimal(int.from_bytes(high_order, "little", signed=False)) # Returns an identical order with the ID changed. def with_id(self, id: int) -> "Order": - return Order(id=id, side=self.side, price=self.price, quantity=self.quantity, - client_id=self.client_id, owner=self.owner, order_type=self.order_type, reduce_only=self.reduce_only) + return Order( + id=id, + side=self.side, + price=self.price, + quantity=self.quantity, + client_id=self.client_id, + owner=self.owner, + order_type=self.order_type, + reduce_only=self.reduce_only, + ) # Returns an identical order with the Client ID changed. def with_client_id(self, client_id: int) -> "Order": - return Order(id=self.id, side=self.side, price=self.price, quantity=self.quantity, - client_id=client_id, owner=self.owner, order_type=self.order_type, reduce_only=self.reduce_only) + return Order( + id=self.id, + side=self.side, + price=self.price, + quantity=self.quantity, + client_id=client_id, + owner=self.owner, + order_type=self.order_type, + reduce_only=self.reduce_only, + ) # Returns an identical order with the price changed. def with_price(self, price: Decimal) -> "Order": - return Order(id=self.id, side=self.side, price=price, quantity=self.quantity, - client_id=self.client_id, owner=self.owner, order_type=self.order_type, reduce_only=self.reduce_only) + return Order( + id=self.id, + side=self.side, + price=price, + quantity=self.quantity, + client_id=self.client_id, + owner=self.owner, + order_type=self.order_type, + reduce_only=self.reduce_only, + ) # Returns an identical order with the quantity changed. def with_quantity(self, quantity: Decimal) -> "Order": - return Order(id=self.id, side=self.side, price=self.price, quantity=quantity, - client_id=self.client_id, owner=self.owner, order_type=self.order_type, reduce_only=self.reduce_only) + return Order( + id=self.id, + side=self.side, + price=self.price, + quantity=quantity, + client_id=self.client_id, + owner=self.owner, + order_type=self.order_type, + reduce_only=self.reduce_only, + ) # Returns an identical order with the owner changed. def with_owner(self, owner: PublicKey) -> "Order": - return Order(id=self.id, side=self.side, price=self.price, quantity=self.quantity, - client_id=self.client_id, owner=owner, order_type=self.order_type, reduce_only=self.reduce_only) + return Order( + id=self.id, + side=self.side, + price=self.price, + quantity=self.quantity, + client_id=self.client_id, + owner=owner, + order_type=self.order_type, + reduce_only=self.reduce_only, + ) @staticmethod def from_serum_order(serum_order: PySerumOrder) -> "Order": price = Decimal(serum_order.info.price) quantity = Decimal(serum_order.info.size) side = Side.from_value(serum_order.side) - order = Order(id=serum_order.order_id, side=side, price=price, quantity=quantity, - client_id=serum_order.client_id, owner=serum_order.open_order_address, - order_type=OrderType.UNKNOWN) + order = Order( + id=serum_order.order_id, + side=side, + price=price, + quantity=quantity, + client_id=serum_order.client_id, + owner=serum_order.open_order_address, + order_type=OrderType.UNKNOWN, + ) return order @staticmethod - def from_basic_info(side: Side, price: Decimal, quantity: Decimal, order_type: OrderType = OrderType.UNKNOWN, reduce_only: bool = False) -> "Order": - order = Order(id=0, side=side, price=price, quantity=quantity, client_id=0, - owner=SYSTEM_PROGRAM_ADDRESS, order_type=order_type, reduce_only=reduce_only) + def from_basic_info( + side: Side, + price: Decimal, + quantity: Decimal, + order_type: OrderType = OrderType.UNKNOWN, + reduce_only: bool = False, + ) -> "Order": + order = Order( + id=0, + side=side, + price=price, + quantity=quantity, + client_id=0, + owner=SYSTEM_PROGRAM_ADDRESS, + order_type=order_type, + reduce_only=reduce_only, + ) return order @staticmethod - def from_ids(id: int, client_id: int, side: Side = Side.BUY, price: Decimal = Decimal(0), quantity: Decimal = Decimal(0)) -> "Order": - return Order(id=id, client_id=client_id, owner=SYSTEM_PROGRAM_ADDRESS, side=side, price=price, quantity=quantity, order_type=OrderType.UNKNOWN, reduce_only=False) + def from_ids( + id: int, + client_id: int, + side: Side = Side.BUY, + price: Decimal = Decimal(0), + quantity: Decimal = Decimal(0), + ) -> "Order": + return Order( + id=id, + client_id=client_id, + owner=SYSTEM_PROGRAM_ADDRESS, + side=side, + price=price, + quantity=quantity, + order_type=OrderType.UNKNOWN, + reduce_only=False, + ) def __str__(self) -> str: owner: str = "" @@ -222,7 +297,13 @@ class Order: class OrderBook: - def __init__(self, symbol: str, lot_size_converter: LotSizeConverter, bids: typing.Sequence[Order], asks: typing.Sequence[Order]) -> None: + def __init__( + self, + symbol: str, + lot_size_converter: LotSizeConverter, + bids: typing.Sequence[Order], + asks: typing.Sequence[Order], + ) -> None: self.symbol: str = symbol self.__lot_size_converter: LotSizeConverter = lot_size_converter self.__bids: typing.Sequence[Order] = [] @@ -236,7 +317,7 @@ class OrderBook: @bids.setter def bids(self, bids: typing.Sequence[Order]) -> None: - """ Sort bids high to low, so best bid is at index 0 """ + """Sort bids high to low, so best bid is at index 0""" bids_list: typing.List[Order] = list(bids) bids_list.sort(key=lambda order: order.id, reverse=True) self.__bids = bids_list @@ -247,7 +328,7 @@ class OrderBook: @asks.setter def asks(self, asks: typing.Sequence[Order]) -> None: - """ Sets asks low to high, so best ask is at index 0""" + """Sets asks low to high, so best ask is at index 0""" asks_list: typing.List[Order] = list(asks) asks_list.sort(key=lambda order: order.id) self.__asks = asks_list @@ -298,14 +379,16 @@ class OrderBook: "owner": "Owner", "side": "Side", "price": "Price", - "quantity": "Quantity" + "quantity": "Quantity", } frame: pandas.DataFrame = pandas.DataFrame([*reversed(self.bids), *self.asks]) frame = frame.drop(["order_type"], axis=1) frame = frame.rename(mapper=column_mapper, axis=1, copy=True) frame["Price"] = pandas.to_numeric(frame["Price"]) - frame["QuantityLots"] = frame["Quantity"].apply(self.__lot_size_converter.base_size_number_to_lots) + frame["QuantityLots"] = frame["Quantity"].apply( + self.__lot_size_converter.base_size_number_to_lots + ) frame["Quantity"] = pandas.to_numeric(frame["Quantity"]) frame["PriceLots"] = pandas.to_numeric(frame["Id"].apply(Order.read_price)) frame["SequenceNumber"] = frame["Id"].apply(Order.read_sequence_number) @@ -322,7 +405,14 @@ class OrderBook: def to_l2_dataframe(self) -> pandas.DataFrame: frame: pandas.DataFrame = self.to_dataframe() - return frame.groupby("Price").agg({"PriceLots": "first", "Side": "first", "Quantity": "sum", "QuantityLots": "sum"}) + return frame.groupby("Price").agg( + { + "PriceLots": "first", + "Side": "first", + "Quantity": "sum", + "QuantityLots": "sum", + } + ) def to_l3_dataframe(self) -> pandas.DataFrame: return self.to_dataframe() @@ -332,6 +422,7 @@ class OrderBook: quantity = f"{order.quantity:,.8f}" price = f"{order.price:,.8f}" return f"{order.side} {quantity:>20} at {price:>20}" + orders_to_show = 5 lines = [] for counter in range(orders_to_show): @@ -342,7 +433,7 @@ class OrderBook: text = "\n\t".join(lines) spread_description = "N/A" if self.spread != 0 and self.top_bid is not None: - spread_percentage = (self.spread / self.top_bid.price) + spread_percentage = self.spread / self.top_bid.price spread_description = f"{self.spread:,.8f}, {spread_percentage:,.3%}" return f"ยซ OrderBook {self.symbol} [spread: {spread_description}]\n\t{text}\nยป" diff --git a/mango/output.py b/mango/output.py index 9d300fe..9d67b3b 100644 --- a/mango/output.py +++ b/mango/output.py @@ -44,11 +44,39 @@ class OutputFormattter: format: OutputFormat def __to_json(self, obj: typing.Any) -> str: - return json.dumps(jsons.dump(obj, strip_attr=("data", "_logger", "lot_size_converter", "tokens", "tokens_by_index", "slots", "base_tokens", "base_tokens_by_index", "oracles", "oracles_by_index", "spot_markets", "spot_markets_by_index", "perp_markets", "perp_markets_by_index", "shared_quote_token", "liquidity_incentive_token"), - key_transformer=jsons.KEY_TRANSFORMER_CAMELCASE), sort_keys=True, indent=4) + return json.dumps( + jsons.dump( + obj, + strip_attr=( + "data", + "_logger", + "lot_size_converter", + "tokens", + "tokens_by_index", + "slots", + "base_tokens", + "base_tokens_by_index", + "oracles", + "oracles_by_index", + "spot_markets", + "spot_markets_by_index", + "perp_markets", + "perp_markets_by_index", + "shared_quote_token", + "liquidity_incentive_token", + ), + key_transformer=jsons.KEY_TRANSFORMER_CAMELCASE, + ), + sort_keys=True, + indent=4, + ) def out(self, *obj: typing.Any) -> None: - if len(obj) == 1 and isinstance(obj[0], collections.abc.Sequence) and not isinstance(obj[0], str): + if ( + len(obj) == 1 + and isinstance(obj[0], collections.abc.Sequence) + and not isinstance(obj[0], str) + ): for item in obj[0]: self.single_out(item) elif isinstance(obj[0], str): @@ -66,8 +94,32 @@ class OutputFormattter: def multi_out(self, *obj: typing.Any) -> None: if self.format == OutputFormat.JSON: - json_value: str = json.dumps(jsons.dump(obj, strip_attr=("data", "_logger", "lot_size_converter", "tokens", "tokens_by_index", "slots", "base_tokens", "base_tokens_by_index", "oracles", "oracles_by_index", "spot_markets", "spot_markets_by_index", "perp_markets", "perp_markets_by_index", "shared_quote_token", "liquidity_incentive_token"), - key_transformer=jsons.KEY_TRANSFORMER_CAMELCASE), sort_keys=True, indent=4) + json_value: str = json.dumps( + jsons.dump( + obj, + strip_attr=( + "data", + "_logger", + "lot_size_converter", + "tokens", + "tokens_by_index", + "slots", + "base_tokens", + "base_tokens_by_index", + "oracles", + "oracles_by_index", + "spot_markets", + "spot_markets_by_index", + "perp_markets", + "perp_markets_by_index", + "shared_quote_token", + "liquidity_incentive_token", + ), + key_transformer=jsons.KEY_TRANSFORMER_CAMELCASE, + ), + sort_keys=True, + indent=4, + ) print(json_value) else: print(*obj) diff --git a/mango/ownedinstrumentvalue.py b/mango/ownedinstrumentvalue.py index a44ed2e..fcfcb37 100644 --- a/mango/ownedinstrumentvalue.py +++ b/mango/ownedinstrumentvalue.py @@ -28,6 +28,7 @@ from .instrumentvalue import InstrumentValue # token mints and values are given separate from the owner `PublicKey` - we can package them # together in this `OwnedInstrumentValue` class. + class OwnedInstrumentValue: def __init__(self, owner: PublicKey, token_value: InstrumentValue) -> None: self._logger: logging.Logger = logging.getLogger(self.__class__.__name__) @@ -35,7 +36,9 @@ class OwnedInstrumentValue: self.token_value = token_value @staticmethod - def find_by_owner(values: typing.Sequence["OwnedInstrumentValue"], owner: PublicKey) -> "OwnedInstrumentValue": + def find_by_owner( + values: typing.Sequence["OwnedInstrumentValue"], owner: PublicKey + ) -> "OwnedInstrumentValue": found = [value for value in values if value.owner == owner] if len(found) == 0: raise Exception(f"Owner '{owner}' not found in: {values}") @@ -46,12 +49,17 @@ class OwnedInstrumentValue: return found[0] @staticmethod - def changes(before: typing.Sequence["OwnedInstrumentValue"], after: typing.Sequence["OwnedInstrumentValue"]) -> typing.Sequence["OwnedInstrumentValue"]: + def changes( + before: typing.Sequence["OwnedInstrumentValue"], + after: typing.Sequence["OwnedInstrumentValue"], + ) -> typing.Sequence["OwnedInstrumentValue"]: changes: typing.List[OwnedInstrumentValue] = [] for before_value in before: after_value = OwnedInstrumentValue.find_by_owner(after, before_value.owner) - token_value = InstrumentValue(before_value.token_value.token, - after_value.token_value.value - before_value.token_value.value) + token_value = InstrumentValue( + before_value.token_value.token, + after_value.token_value.value - before_value.token_value.value, + ) result = OwnedInstrumentValue(before_value.owner, token_value) changes += [result] diff --git a/mango/parse_account_info_to_orders.py b/mango/parse_account_info_to_orders.py index d18a82d..bb8f60b 100644 --- a/mango/parse_account_info_to_orders.py +++ b/mango/parse_account_info_to_orders.py @@ -30,9 +30,15 @@ from .orders import Order # It's here on its own because putting it in orders.py caused a circular reference and I couldn't think # of a better place. # -def parse_account_info_to_orders(account_info: AccountInfo, pyserum_market: PySerumMarket) -> typing.Sequence[Order]: - serum_orderbook_side = PySerumOrderBook.from_bytes(pyserum_market.state, account_info.data) - orders: typing.List[Order] = list(map(Order.from_serum_order, serum_orderbook_side.orders())) +def parse_account_info_to_orders( + account_info: AccountInfo, pyserum_market: PySerumMarket +) -> typing.Sequence[Order]: + serum_orderbook_side = PySerumOrderBook.from_bytes( + pyserum_market.state, account_info.data + ) + orders: typing.List[Order] = list( + map(Order.from_serum_order, serum_orderbook_side.orders()) + ) if serum_orderbook_side._is_bids: orders.reverse() diff --git a/mango/perpaccount.py b/mango/perpaccount.py index 00b22ec..f213335 100644 --- a/mango/perpaccount.py +++ b/mango/perpaccount.py @@ -29,11 +29,22 @@ from .token import Instrument, Token # Perp accounts aren't directly addressable. They exist as a sub-object of a full Mango `Account` object. # class PerpAccount: - def __init__(self, base_position: Decimal, quote_position: Decimal, long_settled_funding: Decimal, - short_settled_funding: Decimal, bids_quantity: Decimal, asks_quantity: Decimal, - taker_base: Decimal, taker_quote: Decimal, mngo_accrued: InstrumentValue, - open_orders: PerpOpenOrders, lot_size_converter: LotSizeConverter, - base_token_value: InstrumentValue, quote_position_raw: Decimal) -> None: + def __init__( + self, + base_position: Decimal, + quote_position: Decimal, + long_settled_funding: Decimal, + short_settled_funding: Decimal, + bids_quantity: Decimal, + asks_quantity: Decimal, + taker_base: Decimal, + taker_quote: Decimal, + mngo_accrued: InstrumentValue, + open_orders: PerpOpenOrders, + lot_size_converter: LotSizeConverter, + base_token_value: InstrumentValue, + quote_position_raw: Decimal, + ) -> None: self.base_position: Decimal = base_position self.quote_position: Decimal = quote_position self.long_settled_funding: Decimal = long_settled_funding @@ -49,7 +60,14 @@ class PerpAccount: self.quote_position_raw: Decimal = quote_position_raw @staticmethod - def from_layout(layout: typing.Any, base_token: Instrument, quote_token: Token, open_orders: PerpOpenOrders, lot_size_converter: LotSizeConverter, mngo_token: Token) -> "PerpAccount": + def from_layout( + layout: typing.Any, + base_token: Instrument, + quote_token: Token, + open_orders: PerpOpenOrders, + lot_size_converter: LotSizeConverter, + mngo_token: Token, + ) -> "PerpAccount": base_position: Decimal = layout.base_position quote_position: Decimal = layout.quote_position long_settled_funding: Decimal = layout.long_settled_funding @@ -59,20 +77,45 @@ class PerpAccount: taker_base: Decimal = layout.taker_base taker_quote: Decimal = layout.taker_quote mngo_accrued_raw: Decimal = layout.mngo_accrued - mngo_accrued: InstrumentValue = InstrumentValue(mngo_token, mngo_token.shift_to_decimals(mngo_accrued_raw)) + mngo_accrued: InstrumentValue = InstrumentValue( + mngo_token, mngo_token.shift_to_decimals(mngo_accrued_raw) + ) - base_position_raw = (base_position + taker_base) * lot_size_converter.base_lot_size - base_token_value: InstrumentValue = InstrumentValue(base_token, base_token.shift_to_decimals(base_position_raw)) + base_position_raw = ( + base_position + taker_base + ) * lot_size_converter.base_lot_size + base_token_value: InstrumentValue = InstrumentValue( + base_token, base_token.shift_to_decimals(base_position_raw) + ) quote_position_raw: Decimal = quote_token.shift_to_decimals(quote_position) - return PerpAccount(base_position, quote_position, long_settled_funding, short_settled_funding, - bids_quantity, asks_quantity, taker_base, taker_quote, mngo_accrued, open_orders, - lot_size_converter, base_token_value, quote_position_raw) + return PerpAccount( + base_position, + quote_position, + long_settled_funding, + short_settled_funding, + bids_quantity, + asks_quantity, + taker_base, + taker_quote, + mngo_accrued, + open_orders, + lot_size_converter, + base_token_value, + quote_position_raw, + ) @property def empty(self) -> bool: - if self.base_position == Decimal(0) and self.quote_position == Decimal(0) and self.long_settled_funding == Decimal(0) and self.short_settled_funding == Decimal(0) and self.mngo_accrued.value == Decimal(0) and self.open_orders.empty: + if ( + self.base_position == Decimal(0) + and self.quote_position == Decimal(0) + and self.long_settled_funding == Decimal(0) + and self.short_settled_funding == Decimal(0) + and self.mngo_accrued.value == Decimal(0) + and self.open_orders.empty + ): return True return False @@ -80,44 +123,66 @@ class PerpAccount: base_position: Decimal = self.base_position unsettled: Decimal if base_position < 0: - unsettled = base_position * (perp_market_cache.short_funding - self.short_settled_funding) + unsettled = base_position * ( + perp_market_cache.short_funding - self.short_settled_funding + ) else: - unsettled = base_position * (perp_market_cache.long_funding - self.long_settled_funding) - return - self.lot_size_converter.quote.shift_to_decimals(unsettled) + unsettled = base_position * ( + perp_market_cache.long_funding - self.long_settled_funding + ) + return -self.lot_size_converter.quote.shift_to_decimals(unsettled) - def asset_value(self, perp_market_cache: PerpMarketCache, price: Decimal) -> Decimal: - base_position: Decimal = self.lot_size_converter.adjust_to_quote_decimals(self.base_position) + def asset_value( + self, perp_market_cache: PerpMarketCache, price: Decimal + ) -> Decimal: + base_position: Decimal = self.lot_size_converter.adjust_to_quote_decimals( + self.base_position + ) value: Decimal = Decimal(0) if base_position > 0: value = base_position * self.lot_size_converter.base_lot_size * price value = self.lot_size_converter.quote.shift_to_decimals(value) - quote_position: Decimal = self.lot_size_converter.quote.shift_to_decimals(self.quote_position) + quote_position: Decimal = self.lot_size_converter.quote.shift_to_decimals( + self.quote_position + ) quote_position += self.unsettled_funding(perp_market_cache) if quote_position > 0: value += quote_position return value - def liability_value(self, perp_market_cache: PerpMarketCache, price: Decimal) -> Decimal: - base_position: Decimal = self.lot_size_converter.adjust_to_quote_decimals(self.base_position) + def liability_value( + self, perp_market_cache: PerpMarketCache, price: Decimal + ) -> Decimal: + base_position: Decimal = self.lot_size_converter.adjust_to_quote_decimals( + self.base_position + ) value: Decimal = Decimal(0) if base_position < 0: value = base_position * self.lot_size_converter.base_lot_size * price value = self.lot_size_converter.quote.shift_to_decimals(value) - quote_position: Decimal = self.lot_size_converter.quote.shift_to_decimals(self.quote_position) + quote_position: Decimal = self.lot_size_converter.quote.shift_to_decimals( + self.quote_position + ) quote_position += self.unsettled_funding(perp_market_cache) if quote_position < 0: value += quote_position return value - def current_value(self, perp_market_cache: PerpMarketCache, price: Decimal) -> Decimal: - base_position: Decimal = self.lot_size_converter.adjust_to_quote_decimals(self.base_position) + def current_value( + self, perp_market_cache: PerpMarketCache, price: Decimal + ) -> Decimal: + base_position: Decimal = self.lot_size_converter.adjust_to_quote_decimals( + self.base_position + ) value: Decimal = base_position * self.lot_size_converter.base_lot_size * price - quote_position: Decimal = self.lot_size_converter.quote.shift_to_decimals(self.quote_position) + quote_position: Decimal = self.lot_size_converter.quote.shift_to_decimals( + self.quote_position + ) quote_position += self.unsettled_funding(perp_market_cache) value += quote_position diff --git a/mango/perpeventqueue.py b/mango/perpeventqueue.py index 9faf275..0a4e8e0 100644 --- a/mango/perpeventqueue.py +++ b/mango/perpeventqueue.py @@ -42,7 +42,9 @@ class PerpEvent(metaclass=abc.ABCMeta): @abc.abstractproperty @property def accounts_to_crank(self) -> typing.Sequence[PublicKey]: - raise NotImplementedError("PerpEvent.accounts_to_crank is not implemented on the base type.") + raise NotImplementedError( + "PerpEvent.accounts_to_crank is not implemented on the base type." + ) def __repr__(self) -> str: return f"{self}" @@ -53,11 +55,24 @@ class PerpEvent(metaclass=abc.ABCMeta): # `PerpOutEvent` stores details of a perp 'fill' event. # class PerpFillEvent(PerpEvent): - def __init__(self, event_type: int, original_index: Decimal, timestamp: datetime, taker_side: Side, - price: Decimal, quantity: Decimal, best_initial: Decimal, maker_slot: Decimal, - maker_out: bool, maker: PublicKey, maker_order_id: Decimal, - maker_client_order_id: Decimal, taker: PublicKey, taker_order_id: Decimal, - taker_client_order_id: Decimal) -> None: + def __init__( + self, + event_type: int, + original_index: Decimal, + timestamp: datetime, + taker_side: Side, + price: Decimal, + quantity: Decimal, + best_initial: Decimal, + maker_slot: Decimal, + maker_out: bool, + maker: PublicKey, + maker_order_id: Decimal, + maker_client_order_id: Decimal, + taker: PublicKey, + taker_order_id: Decimal, + taker_client_order_id: Decimal, + ) -> None: super().__init__(event_type, original_index) self.timestamp: datetime = timestamp self.taker_side: Side = taker_side @@ -99,8 +114,15 @@ class PerpFillEvent(PerpEvent): # `PerpOutEvent` stores details of a perp 'out' event. # class PerpOutEvent(PerpEvent): - def __init__(self, event_type: int, original_index: Decimal, owner: PublicKey, side: Side, - quantity: Decimal, slot: Decimal) -> None: + def __init__( + self, + event_type: int, + original_index: Decimal, + owner: PublicKey, + side: Side, + quantity: Decimal, + slot: Decimal, + ) -> None: super().__init__(event_type, original_index) self.owner: PublicKey = owner self.side: Side = side @@ -120,9 +142,18 @@ class PerpOutEvent(PerpEvent): # `PerpLiquidateEvent` stores details of a perp 'liquidate' event. # class PerpLiquidateEvent(PerpEvent): - def __init__(self, event_type: int, original_index: Decimal, timestamp: datetime, seq_num: Decimal, - liquidatee: PublicKey, liquidator: PublicKey, price: Decimal, quantity: Decimal, - liquidation_fee: Decimal) -> None: + def __init__( + self, + event_type: int, + original_index: Decimal, + timestamp: datetime, + seq_num: Decimal, + liquidatee: PublicKey, + liquidator: PublicKey, + price: Decimal, + quantity: Decimal, + liquidation_fee: Decimal, + ) -> None: super().__init__(event_type, original_index) self.timestamp: datetime = timestamp self.seq_num: Decimal = seq_num @@ -146,7 +177,9 @@ class PerpLiquidateEvent(PerpEvent): # the event queue data is upgraded before this code. # class PerpUnknownEvent(PerpEvent): - def __init__(self, event_type: int, original_index: Decimal, owner: PublicKey) -> None: + def __init__( + self, event_type: int, original_index: Decimal, owner: PublicKey + ) -> None: super().__init__(event_type, original_index) self.owner: PublicKey = owner @@ -162,25 +195,61 @@ class PerpUnknownEvent(PerpEvent): # # `event_builder()` takes an event layout and returns a typed `PerpEvent`. # -def event_builder(lot_size_converter: LotSizeConverter, event_layout: typing.Any, original_index: Decimal) -> typing.Optional[PerpEvent]: - if event_layout.event_type == b'\x00': +def event_builder( + lot_size_converter: LotSizeConverter, + event_layout: typing.Any, + original_index: Decimal, +) -> typing.Optional[PerpEvent]: + if event_layout.event_type == b"\x00": if event_layout.maker is None and event_layout.taker is None: return None taker_side: Side = Side.from_value(event_layout.taker_side) - quantity: Decimal = lot_size_converter.base_size_lots_to_number(event_layout.quantity) + quantity: Decimal = lot_size_converter.base_size_lots_to_number( + event_layout.quantity + ) price: Decimal = lot_size_converter.price_lots_to_number(event_layout.price) - return PerpFillEvent(event_layout.event_type, original_index, event_layout.timestamp, taker_side, - price, quantity, event_layout.best_initial, - event_layout.maker_slot, event_layout.maker_out, event_layout.maker, - event_layout.maker_order_id, event_layout.maker_client_order_id, - event_layout.taker, event_layout.taker_order_id, - event_layout.taker_client_order_id) - elif event_layout.event_type == b'\x01': - return PerpOutEvent(event_layout.event_type, original_index, event_layout.owner, event_layout.side, event_layout.quantity, event_layout.slot) - elif event_layout.event_type == b'\x02': - return PerpLiquidateEvent(event_layout.event_type, original_index, event_layout.timestamp, event_layout.seq_num, event_layout.liquidatee, event_layout.liquidator, event_layout.price, event_layout.quantity, event_layout.liquidation_fee) + return PerpFillEvent( + event_layout.event_type, + original_index, + event_layout.timestamp, + taker_side, + price, + quantity, + event_layout.best_initial, + event_layout.maker_slot, + event_layout.maker_out, + event_layout.maker, + event_layout.maker_order_id, + event_layout.maker_client_order_id, + event_layout.taker, + event_layout.taker_order_id, + event_layout.taker_client_order_id, + ) + elif event_layout.event_type == b"\x01": + return PerpOutEvent( + event_layout.event_type, + original_index, + event_layout.owner, + event_layout.side, + event_layout.quantity, + event_layout.slot, + ) + elif event_layout.event_type == b"\x02": + return PerpLiquidateEvent( + event_layout.event_type, + original_index, + event_layout.timestamp, + event_layout.seq_num, + event_layout.liquidatee, + event_layout.liquidator, + event_layout.price, + event_layout.quantity, + event_layout.liquidation_fee, + ) else: - return PerpUnknownEvent(event_layout.event_type, original_index, event_layout.owner) + return PerpUnknownEvent( + event_layout.event_type, original_index, event_layout.owner + ) # # ๐Ÿฅญ PerpEventQueue class @@ -189,10 +258,17 @@ def event_builder(lot_size_converter: LotSizeConverter, event_layout: typing.Any # processed by 'consume events' and which are not. # class PerpEventQueue(AddressableAccount): - def __init__(self, account_info: AccountInfo, version: Version, meta_data: Metadata, - head: Decimal, count: Decimal, sequence_number: Decimal, - unprocessed_events: typing.Sequence[PerpEvent], - processed_events: typing.Sequence[PerpEvent]) -> None: + def __init__( + self, + account_info: AccountInfo, + version: Version, + meta_data: Metadata, + head: Decimal, + count: Decimal, + sequence_number: Decimal, + unprocessed_events: typing.Sequence[PerpEvent], + processed_events: typing.Sequence[PerpEvent], + ) -> None: super().__init__(account_info) self.version: Version = version @@ -205,7 +281,12 @@ class PerpEventQueue(AddressableAccount): self.processed_events: typing.Sequence[PerpEvent] = processed_events @staticmethod - def from_layout(layout: typing.Any, account_info: AccountInfo, version: Version, lot_size_converter: LotSizeConverter) -> "PerpEventQueue": + def from_layout( + layout: typing.Any, + account_info: AccountInfo, + version: Version, + lot_size_converter: LotSizeConverter, + ) -> "PerpEventQueue": meta_data: Metadata = Metadata.from_layout(layout.meta_data) head: Decimal = layout.head count: Decimal = layout.count @@ -219,23 +300,38 @@ class PerpEventQueue(AddressableAccount): # Events are stored in a ringbuffer, and the oldest is overwritten when a new event arrives. # Make it a bit simpler to use by splitting at the insertion point and swapping the two pieces # around so that users don't have to do modulo arithmetic on the capacity. - ordered_events = events[int(head):] + events[0:int(head)] + ordered_events = events[int(head) :] + events[0 : int(head)] # Now chop the oldest-to-newest list of events into processed and unprocessed. The `count` # property holds the number of unprocessed events. - unprocessed_events = ordered_events[0:int(count)] - processed_events = ordered_events[int(count):] + unprocessed_events = ordered_events[0 : int(count)] + processed_events = ordered_events[int(count) :] - return PerpEventQueue(account_info, version, meta_data, head, count, seq_num, unprocessed_events, processed_events) + return PerpEventQueue( + account_info, + version, + meta_data, + head, + count, + seq_num, + unprocessed_events, + processed_events, + ) - @ staticmethod - def parse(account_info: AccountInfo, lot_size_converter: LotSizeConverter) -> "PerpEventQueue": + @staticmethod + def parse( + account_info: AccountInfo, lot_size_converter: LotSizeConverter + ) -> "PerpEventQueue": # Data length isn't fixed so can't check we get the right value the way we normally do. layout = layouts.PERP_EVENT_QUEUE.parse(account_info.data) - return PerpEventQueue.from_layout(layout, account_info, Version.V1, lot_size_converter) + return PerpEventQueue.from_layout( + layout, account_info, Version.V1, lot_size_converter + ) - @ staticmethod - def load(context: Context, address: PublicKey, lot_size_converter: LotSizeConverter) -> "PerpEventQueue": + @staticmethod + def load( + context: Context, address: PublicKey, lot_size_converter: LotSizeConverter + ) -> "PerpEventQueue": account_info = AccountInfo.load(context, address) if account_info is None: raise Exception(f"PerpEventQueue account not found at address '{address}'") @@ -257,29 +353,49 @@ class PerpEventQueue(AddressableAccount): return distinct - def events_for_account(self, mango_account_address: PublicKey) -> typing.Sequence[PerpEvent]: + def events_for_account( + self, mango_account_address: PublicKey + ) -> typing.Sequence[PerpEvent]: events: typing.List[PerpEvent] = [] for event in [*self.processed_events, *self.unprocessed_events]: if mango_account_address in event.accounts_to_crank: events += [event] return events - def fills_for_account(self, mango_account_address: PublicKey) -> typing.Sequence[PerpFillEvent]: + def fills_for_account( + self, mango_account_address: PublicKey + ) -> typing.Sequence[PerpFillEvent]: fills: typing.List[PerpFillEvent] = [] for event in self.events_for_account(mango_account_address): if isinstance(event, PerpFillEvent): fills += [event] return fills - @ property + @property def capacity(self) -> int: return len(self.unprocessed_events) + len(self.processed_events) def __str__(self) -> str: - unprocessed_events = "\n ".join([f"{event}".replace("\n", "\n ") - for event in self.unprocessed_events if event is not None]) or "None" - processed_events = "\n ".join([f"{event}".replace("\n", "\n ") - for event in self.processed_events if event is not None]) or "None" + unprocessed_events = ( + "\n ".join( + [ + f"{event}".replace("\n", "\n ") + for event in self.unprocessed_events + if event is not None + ] + ) + or "None" + ) + processed_events = ( + "\n ".join( + [ + f"{event}".replace("\n", "\n ") + for event in self.processed_events + if event is not None + ] + ) + or "None" + ) return f"""ยซ PerpEventQueue [{self.version}] {self.address} {self.meta_data} Head: {self.head} @@ -312,7 +428,9 @@ class UnseenPerpEventChangesTracker: new_sequence_number: Decimal = event_queue.sequence_number if self.last_sequence_number != new_sequence_number: number_of_changes: Decimal = new_sequence_number - self.last_sequence_number - unseen = [*event_queue.processed_events, *event_queue.unprocessed_events][0 - int(number_of_changes):] + unseen = [*event_queue.processed_events, *event_queue.unprocessed_events][ + 0 - int(number_of_changes) : + ] self.last_sequence_number = new_sequence_number return unseen @@ -325,14 +443,20 @@ class UnseenPerpEventChangesTracker: # any new fills are returned. # class UnseenAccountFillEventTracker: - def __init__(self, initial: PerpEventQueue, mango_account_address: PublicKey) -> None: + def __init__( + self, initial: PerpEventQueue, mango_account_address: PublicKey + ) -> None: self.mango_account_address: PublicKey = mango_account_address self.last_sequence_number: Decimal = initial.sequence_number - initial_fills: typing.Sequence[PerpFillEvent] = initial.fills_for_account(mango_account_address) + initial_fills: typing.Sequence[PerpFillEvent] = initial.fills_for_account( + mango_account_address + ) self.last_key: str = initial_fills[-1].key if len(initial_fills) > 0 else "" def unseen(self, event_queue: PerpEventQueue) -> typing.Sequence[PerpEvent]: - fills: typing.Sequence[PerpFillEvent] = event_queue.fills_for_account(self.mango_account_address) + fills: typing.Sequence[PerpFillEvent] = event_queue.fills_for_account( + self.mango_account_address + ) if len(fills) == 0: return [] @@ -349,7 +473,7 @@ class UnseenAccountFillEventTracker: elif last_key_position == len(fills) - 1: unseen = [] else: - unseen = fills[last_key_position + 1:] + unseen = fills[last_key_position + 1 :] self.last_key = unseen[-1].key return unseen diff --git a/mango/perpmarket.py b/mango/perpmarket.py index e321f62..792e133 100644 --- a/mango/perpmarket.py +++ b/mango/perpmarket.py @@ -53,7 +53,12 @@ class FundingRate: to: datetime @staticmethod - def from_stats_data(symbol: str, lot_size_converter: LotSizeConverter, oldest_stats: typing.Dict[str, typing.Any], newest_stats: typing.Dict[str, typing.Any]) -> "FundingRate": + def from_stats_data( + symbol: str, + lot_size_converter: LotSizeConverter, + oldest_stats: typing.Dict[str, typing.Any], + newest_stats: typing.Dict[str, typing.Any], + ) -> "FundingRate": oldest_short_funding = Decimal(oldest_stats["shortFunding"]) oldest_long_funding = Decimal(oldest_stats["longFunding"]) oldest_oracle_price = Decimal(oldest_stats["baseOraclePrice"]) @@ -64,7 +69,9 @@ class FundingRate: newest_oracle_price = Decimal(newest_stats["baseOraclePrice"]) to_timestamp = parser.parse(newest_stats["time"]).replace(microsecond=0) raw_open_interest = Decimal(newest_stats["openInterest"]) - open_interest = lot_size_converter.base_size_lots_to_number(raw_open_interest) / 2 + open_interest = ( + lot_size_converter.base_size_lots_to_number(raw_open_interest) / 2 + ) average_oracle_price = (oldest_oracle_price + newest_oracle_price) / 2 average_oracle_price = newest_oracle_price @@ -73,11 +80,20 @@ class FundingRate: end_funding = (newest_long_funding + newest_short_funding) / 2 funding_difference = end_funding - start_funding - funding_in_quote_decimals = lot_size_converter.quote.shift_to_decimals(funding_difference) + funding_in_quote_decimals = lot_size_converter.quote.shift_to_decimals( + funding_difference + ) base_price_in_base_lots = average_oracle_price * lot_size_converter.lot_size funding_rate = funding_in_quote_decimals / base_price_in_base_lots - return FundingRate(symbol=symbol, rate=funding_rate, oracle_price=average_oracle_price, open_interest=open_interest, from_=from_timestamp, to=to_timestamp) + return FundingRate( + symbol=symbol, + rate=funding_rate, + oracle_price=average_oracle_price, + open_interest=open_interest, + from_=from_timestamp, + to=to_timestamp, + ) def __str__(self) -> str: return f"ยซ FundingRate {self.symbol} {self.rate:,.8%}, open interest: {self.open_interest:,.8f} from: {self.from_} to {self.to} ยป" @@ -91,12 +107,29 @@ class FundingRate: # This class encapsulates our knowledge of a Mango perps market. # class PerpMarket(LoadedMarket): - def __init__(self, mango_program_address: PublicKey, address: PublicKey, base: Instrument, quote: Token, - underlying_perp_market: PerpMarketDetails) -> None: - super().__init__(mango_program_address, address, InventorySource.ACCOUNT, base, quote, RaisingLotSizeConverter()) + def __init__( + self, + mango_program_address: PublicKey, + address: PublicKey, + base: Instrument, + quote: Token, + underlying_perp_market: PerpMarketDetails, + ) -> None: + super().__init__( + mango_program_address, + address, + InventorySource.ACCOUNT, + base, + quote, + RaisingLotSizeConverter(), + ) self.underlying_perp_market: PerpMarketDetails = underlying_perp_market self.lot_size_converter: LotSizeConverter = LotSizeConverter( - base, underlying_perp_market.base_lot_size, quote, underlying_perp_market.quote_lot_size) + base, + underlying_perp_market.base_lot_size, + quote, + underlying_perp_market.quote_lot_size, + ) @property def symbol(self) -> str: @@ -118,36 +151,58 @@ class PerpMarket(LoadedMarket): def event_queue_address(self) -> PublicKey: return self.underlying_perp_market.event_queue - def parse_account_info_to_orders(self, account_info: AccountInfo) -> typing.Sequence[Order]: - side: PerpOrderBookSide = PerpOrderBookSide.parse(account_info, self.underlying_perp_market) + def parse_account_info_to_orders( + self, account_info: AccountInfo + ) -> typing.Sequence[Order]: + side: PerpOrderBookSide = PerpOrderBookSide.parse( + account_info, self.underlying_perp_market + ) return side.orders() def fetch_funding(self, context: Context) -> FundingRate: - stats = context.fetch_stats(f"perp/funding_rate?mangoGroup={self.group.name}&market={self.symbol}") + stats = context.fetch_stats( + f"perp/funding_rate?mangoGroup={self.group.name}&market={self.symbol}" + ) newest_stats = stats[0] oldest_stats = stats[-1] - return FundingRate.from_stats_data(self.symbol, self.lot_size_converter, oldest_stats, newest_stats) + return FundingRate.from_stats_data( + self.symbol, self.lot_size_converter, oldest_stats, newest_stats + ) def unprocessed_events(self, context: Context) -> typing.Sequence[PerpEvent]: - event_queue: PerpEventQueue = PerpEventQueue.load(context, self.event_queue_address, self.lot_size_converter) + event_queue: PerpEventQueue = PerpEventQueue.load( + context, self.event_queue_address, self.lot_size_converter + ) return event_queue.unprocessed_events def observe_events(self, context: Context, interval: int = 30) -> DisposingSubject: perp_event_queue: PerpEventQueue = PerpEventQueue.load( - context, self.underlying_perp_market.event_queue, self.lot_size_converter) - perp_splitter: UnseenPerpEventChangesTracker = UnseenPerpEventChangesTracker(perp_event_queue) + context, self.underlying_perp_market.event_queue, self.lot_size_converter + ) + perp_splitter: UnseenPerpEventChangesTracker = UnseenPerpEventChangesTracker( + perp_event_queue + ) fill_events = DisposingSubject() - disposable_subscription = rx.interval(interval).pipe( - ops.observe_on(context.create_thread_pool_scheduler()), - ops.start_with(-1), - ops.map(lambda _: PerpEventQueue.load( - context, self.underlying_perp_market.event_queue, self.lot_size_converter)), - ops.flat_map(perp_splitter.unseen), - ops.catch(observable_pipeline_error_reporter), - ops.retry() - ).subscribe(fill_events) + disposable_subscription = ( + rx.interval(interval) + .pipe( + ops.observe_on(context.create_thread_pool_scheduler()), + ops.start_with(-1), + ops.map( + lambda _: PerpEventQueue.load( + context, + self.underlying_perp_market.event_queue, + self.lot_size_converter, + ) + ), + ops.flat_map(perp_splitter.unseen), + ops.catch(observable_pipeline_error_reporter), + ops.retry(), + ) + .subscribe(fill_events) + ) fill_events.add_disposable(disposable_subscription) return fill_events @@ -163,19 +218,44 @@ class PerpMarket(LoadedMarket): # This class holds information to load a `PerpMarket` object but doesn't automatically load it. # class PerpMarketStub(Market): - def __init__(self, mango_program_address: PublicKey, address: PublicKey, base: Instrument, quote: Token, - group_address: PublicKey) -> None: - super().__init__(mango_program_address, address, InventorySource.ACCOUNT, base, quote, RaisingLotSizeConverter()) + def __init__( + self, + mango_program_address: PublicKey, + address: PublicKey, + base: Instrument, + quote: Token, + group_address: PublicKey, + ) -> None: + super().__init__( + mango_program_address, + address, + InventorySource.ACCOUNT, + base, + quote, + RaisingLotSizeConverter(), + ) self.group_address: PublicKey = group_address - def load(self, context: Context, group: typing.Optional[Group] = None) -> PerpMarket: + def load( + self, context: Context, group: typing.Optional[Group] = None + ) -> PerpMarket: actual_group: Group = group or Group.load(context, self.group_address) - underlying_perp_market: PerpMarketDetails = PerpMarketDetails.load(context, self.address, actual_group) - return PerpMarket(self.program_address, self.address, self.base, self.quote, underlying_perp_market) + underlying_perp_market: PerpMarketDetails = PerpMarketDetails.load( + context, self.address, actual_group + ) + return PerpMarket( + self.program_address, + self.address, + self.base, + self.quote, + underlying_perp_market, + ) @property def symbol(self) -> str: return f"{self.base.symbol}-PERP" def __str__(self) -> str: - return f"ยซ PerpMarketStub {self.symbol} {self.address} [{self.program_address}] ยป" + return ( + f"ยซ PerpMarketStub {self.symbol} {self.address} [{self.program_address}] ยป" + ) diff --git a/mango/perpmarketdetails.py b/mango/perpmarketdetails.py index 5ae62b7..0013315 100644 --- a/mango/perpmarketdetails.py +++ b/mango/perpmarketdetails.py @@ -32,9 +32,16 @@ from .version import Version class LiquidityMiningInfo: - def __init__(self, version: Version, rate: Decimal, max_depth_bps: Decimal, period_start: datetime, - target_period_length: timedelta, mngo_left: InstrumentValue, - mngo_per_period: InstrumentValue) -> None: + def __init__( + self, + version: Version, + rate: Decimal, + max_depth_bps: Decimal, + period_start: datetime, + target_period_length: timedelta, + mngo_left: InstrumentValue, + mngo_per_period: InstrumentValue, + ) -> None: self.version: Version = version self.rate: Decimal = rate @@ -45,16 +52,31 @@ class LiquidityMiningInfo: self.mngo_per_period: InstrumentValue = mngo_per_period @staticmethod - def from_layout(layout: typing.Any, version: Version, mngo: Token) -> "LiquidityMiningInfo": + def from_layout( + layout: typing.Any, version: Version, mngo: Token + ) -> "LiquidityMiningInfo": rate: Decimal = layout.rate max_depth_bps: Decimal = layout.max_depth_bps period_start: datetime = layout.period_start - target_period_length: timedelta = timedelta(seconds=float(layout.target_period_length)) - mngo_left: InstrumentValue = InstrumentValue(mngo, mngo.shift_to_decimals(layout.mngo_left)) - mngo_per_period: InstrumentValue = InstrumentValue(mngo, mngo.shift_to_decimals(layout.mngo_per_period)) + target_period_length: timedelta = timedelta( + seconds=float(layout.target_period_length) + ) + mngo_left: InstrumentValue = InstrumentValue( + mngo, mngo.shift_to_decimals(layout.mngo_left) + ) + mngo_per_period: InstrumentValue = InstrumentValue( + mngo, mngo.shift_to_decimals(layout.mngo_per_period) + ) - return LiquidityMiningInfo(version, rate, max_depth_bps, period_start, target_period_length, - mngo_left, mngo_per_period) + return LiquidityMiningInfo( + version, + rate, + max_depth_bps, + period_start, + target_period_length, + mngo_left, + mngo_per_period, + ) def __str__(self) -> str: # Some calculations here are basd on this message from 0xHiroku#0491 on Discord: @@ -72,13 +94,19 @@ class LiquidityMiningInfo: elapsed_seconds: float = elapsed.total_seconds() rounded_elapsed: timedelta = timedelta(seconds=int(elapsed_seconds)) estimated_duration_seconds: float = elapsed_seconds - estimated_duration: timedelta = timedelta(seconds=int(estimated_duration_seconds)) - estimated_remaining_seconds: float = estimated_duration_seconds - elapsed_seconds - estimated_remaining: timedelta = timedelta(seconds=int(estimated_remaining_seconds)) + estimated_duration: timedelta = timedelta( + seconds=int(estimated_duration_seconds) + ) + estimated_remaining_seconds: float = ( + estimated_duration_seconds - elapsed_seconds + ) + estimated_remaining: timedelta = timedelta( + seconds=int(estimated_remaining_seconds) + ) estimated_end: datetime = now + estimated_remaining if self.mngo_per_period.value != 0: proportion_distributed = mngo_distributed.value / self.mngo_per_period.value - estimated_duration_seconds = (elapsed_seconds / float(proportion_distributed)) + estimated_duration_seconds = elapsed_seconds / float(proportion_distributed) estimated_duration = timedelta(seconds=int(estimated_duration_seconds)) estimated_remaining_seconds = estimated_duration_seconds - elapsed_seconds estimated_remaining = timedelta(seconds=int(estimated_remaining_seconds)) @@ -104,12 +132,26 @@ class LiquidityMiningInfo: # `PerpMarketDetails` holds details of a particular perp market. # class PerpMarketDetails(AddressableAccount): - def __init__(self, account_info: AccountInfo, version: Version, - meta_data: Metadata, group: Group, bids: PublicKey, asks: PublicKey, - event_queue: PublicKey, base_lot_size: Decimal, quote_lot_size: Decimal, long_funding: Decimal, - short_funding: Decimal, open_interest: Decimal, last_updated: datetime, seq_num: Decimal, - fees_accrued: Decimal, liquidity_mining_info: LiquidityMiningInfo, - mngo_vault: PublicKey) -> None: + def __init__( + self, + account_info: AccountInfo, + version: Version, + meta_data: Metadata, + group: Group, + bids: PublicKey, + asks: PublicKey, + event_queue: PublicKey, + base_lot_size: Decimal, + quote_lot_size: Decimal, + long_funding: Decimal, + short_funding: Decimal, + open_interest: Decimal, + last_updated: datetime, + seq_num: Decimal, + fees_accrued: Decimal, + liquidity_mining_info: LiquidityMiningInfo, + mngo_vault: PublicKey, + ) -> None: super().__init__(account_info) self.version: Version = version @@ -131,7 +173,9 @@ class PerpMarketDetails(AddressableAccount): slot: GroupSlot = group.slot_by_perp_market_address(self.address) if slot is None: - raise Exception(f"Could not find slot for perp market {self.address} in group {group.address}.") + raise Exception( + f"Could not find slot for perp market {self.address} in group {group.address}." + ) self.market_index: int = slot.index @@ -140,7 +184,9 @@ class PerpMarketDetails(AddressableAccount): self.quote_token: TokenBank = group.shared_quote @staticmethod - def from_layout(layout: typing.Any, account_info: AccountInfo, version: Version, group: Group) -> "PerpMarketDetails": + def from_layout( + layout: typing.Any, account_info: AccountInfo, version: Version, group: Group + ) -> "PerpMarketDetails": meta_data = Metadata.from_layout(layout.meta_data) bids: PublicKey = layout.bids asks: PublicKey = layout.asks @@ -156,20 +202,37 @@ class PerpMarketDetails(AddressableAccount): fees_accrued: Decimal = layout.fees_accrued liquidity_mining_info: LiquidityMiningInfo = LiquidityMiningInfo.from_layout( - layout.liquidity_mining_info, Version.V1, group.liquidity_incentive_token) + layout.liquidity_mining_info, Version.V1, group.liquidity_incentive_token + ) mngo_vault: PublicKey = layout.mngo_vault - return PerpMarketDetails(account_info, version, meta_data, group, bids, asks, event_queue, - base_lot_size, quote_lot_size, long_funding, short_funding, open_interest, - last_updated, seq_num, fees_accrued, liquidity_mining_info, - mngo_vault) + return PerpMarketDetails( + account_info, + version, + meta_data, + group, + bids, + asks, + event_queue, + base_lot_size, + quote_lot_size, + long_funding, + short_funding, + open_interest, + last_updated, + seq_num, + fees_accrued, + liquidity_mining_info, + mngo_vault, + ) @staticmethod def parse(account_info: AccountInfo, group: Group) -> "PerpMarketDetails": data = account_info.data if len(data) != layouts.PERP_MARKET.sizeof(): raise Exception( - f"PerpMarketDetails data length ({len(data)}) does not match expected size ({layouts.PERP_MARKET.sizeof()})") + f"PerpMarketDetails data length ({len(data)}) does not match expected size ({layouts.PERP_MARKET.sizeof()})" + ) layout = layouts.PERP_MARKET.parse(data) return PerpMarketDetails.from_layout(layout, account_info, Version.V1, group) @@ -178,11 +241,15 @@ class PerpMarketDetails(AddressableAccount): def load(context: Context, address: PublicKey, group: Group) -> "PerpMarketDetails": account_info = AccountInfo.load(context, address) if account_info is None: - raise Exception(f"PerpMarketDetails account not found at address '{address}'") + raise Exception( + f"PerpMarketDetails account not found at address '{address}'" + ) return PerpMarketDetails.parse(account_info, group) def __str__(self) -> str: - liquidity_mining_info: str = f"{self.liquidity_mining_info}".replace("\n", "\n ") + liquidity_mining_info: str = f"{self.liquidity_mining_info}".replace( + "\n", "\n " + ) return f"""ยซ PerpMarketDetails {self.version} [{self.address}] {self.meta_data} Group: {self.group.address} diff --git a/mango/perpmarketoperations.py b/mango/perpmarketoperations.py index e7b0335..fa2be25 100644 --- a/mango/perpmarketoperations.py +++ b/mango/perpmarketoperations.py @@ -24,7 +24,13 @@ from .combinableinstructions import CombinableInstructions from .constants import SYSTEM_PROGRAM_ADDRESS from .context import Context from .group import Group -from .instructions import build_cancel_perp_order_instructions, build_mango_consume_events_instructions, build_cancel_all_perp_orders_instructions, build_place_perp_order_instructions, build_redeem_accrued_mango_instructions +from .instructions import ( + build_cancel_perp_order_instructions, + build_mango_consume_events_instructions, + build_cancel_all_perp_orders_instructions, + build_place_perp_order_instructions, + build_redeem_accrued_mango_instructions, +) from .marketoperations import MarketInstructionBuilder, MarketOperations from .orders import Order, OrderBook from .perpmarket import PerpMarket @@ -42,8 +48,14 @@ from .wallet import Wallet # on initial setup in the `load()` method. # class PerpMarketInstructionBuilder(MarketInstructionBuilder): - def __init__(self, context: Context, wallet: Wallet, perp_market: PerpMarket, - group: Group, account: Account) -> None: + def __init__( + self, + context: Context, + wallet: Wallet, + perp_market: PerpMarket, + group: Group, + account: Account, + ) -> None: super().__init__() self.context: Context = context self.wallet: Wallet = wallet @@ -53,27 +65,63 @@ class PerpMarketInstructionBuilder(MarketInstructionBuilder): self.mngo_token_bank: TokenBank = self.group.liquidity_incentive_token_bank @staticmethod - def load(context: Context, wallet: Wallet, perp_market: PerpMarket, group: Group, account: Account) -> "PerpMarketInstructionBuilder": - return PerpMarketInstructionBuilder(context, wallet, perp_market, group, account) + def load( + context: Context, + wallet: Wallet, + perp_market: PerpMarket, + group: Group, + account: Account, + ) -> "PerpMarketInstructionBuilder": + return PerpMarketInstructionBuilder( + context, wallet, perp_market, group, account + ) - def build_cancel_order_instructions(self, order: Order, ok_if_missing: bool = False) -> CombinableInstructions: + def build_cancel_order_instructions( + self, order: Order, ok_if_missing: bool = False + ) -> CombinableInstructions: if self.perp_market.underlying_perp_market is None: - raise Exception(f"PerpMarket {self.perp_market.symbol} has not been loaded.") + raise Exception( + f"PerpMarket {self.perp_market.symbol} has not been loaded." + ) return build_cancel_perp_order_instructions( - self.context, self.wallet, self.account, self.perp_market.underlying_perp_market, order, ok_if_missing) + self.context, + self.wallet, + self.account, + self.perp_market.underlying_perp_market, + order, + ok_if_missing, + ) def build_place_order_instructions(self, order: Order) -> CombinableInstructions: if self.perp_market.underlying_perp_market is None: - raise Exception(f"PerpMarket {self.perp_market.symbol} has not been loaded.") + raise Exception( + f"PerpMarket {self.perp_market.symbol} has not been loaded." + ) return build_place_perp_order_instructions( - self.context, self.wallet, self.perp_market.underlying_perp_market.group, self.account, self.perp_market.underlying_perp_market, order.price, order.quantity, order.client_id, order.side, order.order_type, order.reduce_only, self.context.reflink) + self.context, + self.wallet, + self.perp_market.underlying_perp_market.group, + self.account, + self.perp_market.underlying_perp_market, + order.price, + order.quantity, + order.client_id, + order.side, + order.order_type, + order.reduce_only, + self.context.reflink, + ) def build_settle_instructions(self) -> CombinableInstructions: return CombinableInstructions.empty() - def build_crank_instructions(self, addresses: typing.Sequence[PublicKey], limit: Decimal = Decimal(32)) -> CombinableInstructions: + def build_crank_instructions( + self, addresses: typing.Sequence[PublicKey], limit: Decimal = Decimal(32) + ) -> CombinableInstructions: if self.perp_market.underlying_perp_market is None: - raise Exception(f"PerpMarket {self.perp_market.symbol} has not been loaded.") + raise Exception( + f"PerpMarket {self.perp_market.symbol} has not been loaded." + ) distinct_addresses: typing.List[PublicKey] = [self.account.address] for address in addresses: @@ -82,22 +130,49 @@ class PerpMarketInstructionBuilder(MarketInstructionBuilder): if len(distinct_addresses) > limit: self._logger.warn( - f"Cranking limited to {limit} of {len(distinct_addresses)} addresses waiting to be cranked.") + f"Cranking limited to {limit} of {len(distinct_addresses)} addresses waiting to be cranked." + ) - limited_addresses = distinct_addresses[0:min(int(limit), len(distinct_addresses))] + limited_addresses = distinct_addresses[ + 0 : min(int(limit), len(distinct_addresses)) + ] limited_addresses.sort(key=encode_public_key_for_sorting) - self._logger.debug(f"About to crank {len(limited_addresses)} addresses: {limited_addresses}") + self._logger.debug( + f"About to crank {len(limited_addresses)} addresses: {limited_addresses}" + ) - return build_mango_consume_events_instructions(self.context, self.group, self.perp_market.underlying_perp_market, limited_addresses, limit) + return build_mango_consume_events_instructions( + self.context, + self.group, + self.perp_market.underlying_perp_market, + limited_addresses, + limit, + ) def build_redeem_instructions(self) -> CombinableInstructions: - return build_redeem_accrued_mango_instructions(self.context, self.wallet, self.perp_market, self.group, self.account, self.mngo_token_bank) + return build_redeem_accrued_mango_instructions( + self.context, + self.wallet, + self.perp_market, + self.group, + self.account, + self.mngo_token_bank, + ) - def build_cancel_all_orders_instructions(self, limit: Decimal = Decimal(32)) -> CombinableInstructions: + def build_cancel_all_orders_instructions( + self, limit: Decimal = Decimal(32) + ) -> CombinableInstructions: if self.perp_market.underlying_perp_market is None: - raise Exception(f"PerpMarket {self.perp_market.symbol} has not been loaded.") + raise Exception( + f"PerpMarket {self.perp_market.symbol} has not been loaded." + ) return build_cancel_all_perp_orders_instructions( - self.context, self.wallet, self.account, self.perp_market.underlying_perp_market, limit) + self.context, + self.wallet, + self.account, + self.perp_market.underlying_perp_market, + limit, + ) def __str__(self) -> str: return """ยซ PerpMarketInstructionBuilder ยป""" @@ -108,12 +183,19 @@ class PerpMarketInstructionBuilder(MarketInstructionBuilder): # This file deals with placing orders for Perps. # class PerpMarketOperations(MarketOperations): - def __init__(self, context: Context, wallet: Wallet, account: Account, - market_instruction_builder: PerpMarketInstructionBuilder) -> None: + def __init__( + self, + context: Context, + wallet: Wallet, + account: Account, + market_instruction_builder: PerpMarketInstructionBuilder, + ) -> None: super().__init__(market_instruction_builder.perp_market) self.context: Context = context self.wallet: Wallet = wallet - self.market_instruction_builder: PerpMarketInstructionBuilder = market_instruction_builder + self.market_instruction_builder: PerpMarketInstructionBuilder = ( + market_instruction_builder + ) self.account: Account = account @property @@ -124,34 +206,50 @@ class PerpMarketOperations(MarketOperations): def market_name(self) -> str: return self.perp_market.symbol - def cancel_order(self, order: Order, ok_if_missing: bool = False) -> typing.Sequence[str]: + def cancel_order( + self, order: Order, ok_if_missing: bool = False + ) -> typing.Sequence[str]: self._logger.info(f"Cancelling {self.market_name} order {order}.") - signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) - cancel: CombinableInstructions = self.market_instruction_builder.build_cancel_order_instructions( - order, ok_if_missing=ok_if_missing) + signers: CombinableInstructions = CombinableInstructions.from_wallet( + self.wallet + ) + cancel: CombinableInstructions = ( + self.market_instruction_builder.build_cancel_order_instructions( + order, ok_if_missing=ok_if_missing + ) + ) crank = self._build_crank(add_self=True) settle = self.market_instruction_builder.build_settle_instructions() return (signers + cancel + crank + settle).execute(self.context) def place_order(self, order: Order, crank_limit: Decimal = Decimal(5)) -> Order: client_id: int = self.context.generate_client_id() - signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) + signers: CombinableInstructions = CombinableInstructions.from_wallet( + self.wallet + ) order_with_client_id: Order = order.with_client_id(client_id) self._logger.info(f"Placing {self.market_name} order {order_with_client_id}.") - place: CombinableInstructions = self.market_instruction_builder.build_place_order_instructions( - order_with_client_id) + place: CombinableInstructions = ( + self.market_instruction_builder.build_place_order_instructions( + order_with_client_id + ) + ) crank = self._build_crank(add_self=True, limit=crank_limit) settle = self.market_instruction_builder.build_settle_instructions() (signers + place + crank + settle).execute(self.context) return order_with_client_id def settle(self) -> typing.Sequence[str]: - signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) + signers: CombinableInstructions = CombinableInstructions.from_wallet( + self.wallet + ) settle = self.market_instruction_builder.build_settle_instructions() return (signers + settle).execute(self.context) def crank(self, limit: Decimal = Decimal(32)) -> typing.Sequence[str]: - signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) + signers: CombinableInstructions = CombinableInstructions.from_wallet( + self.wallet + ) crank = self._build_crank(limit=limit) return (signers + crank).execute(self.context) @@ -168,7 +266,9 @@ class PerpMarketOperations(MarketOperations): orderbook: OrderBook = self.load_orderbook() return orderbook.all_orders_for_owner(self.account.address) - def _build_crank(self, limit: Decimal = Decimal(32), add_self: bool = False) -> CombinableInstructions: + def _build_crank( + self, limit: Decimal = Decimal(32), add_self: bool = False + ) -> CombinableInstructions: accounts_to_crank: typing.List[PublicKey] = [] for event_to_crank in self.perp_market.unprocessed_events(self.context): accounts_to_crank += event_to_crank.accounts_to_crank @@ -180,8 +280,11 @@ class PerpMarketOperations(MarketOperations): return CombinableInstructions.empty() self._logger.debug( - f"Building crank instruction with {len(accounts_to_crank)} public keys, throttled to {limit}") - return self.market_instruction_builder.build_crank_instructions(accounts_to_crank, limit) + f"Building crank instruction with {len(accounts_to_crank)} public keys, throttled to {limit}" + ) + return self.market_instruction_builder.build_crank_instructions( + accounts_to_crank, limit + ) def __str__(self) -> str: return f"""ยซ PerpMarketOperations [{self.market_name}] ยป""" diff --git a/mango/placedorder.py b/mango/placedorder.py index b3f0d7b..bf4461b 100644 --- a/mango/placedorder.py +++ b/mango/placedorder.py @@ -37,7 +37,12 @@ class PlacedOrder: side: Side @staticmethod - def build_from_open_orders_data(free_slot_bits: Decimal, is_bid_bits: Decimal, order_ids: typing.Sequence[Decimal], client_order_ids: typing.Sequence[Decimal]) -> typing.Sequence["PlacedOrder"]: + def build_from_open_orders_data( + free_slot_bits: Decimal, + is_bid_bits: Decimal, + order_ids: typing.Sequence[Decimal], + client_order_ids: typing.Sequence[Decimal], + ) -> typing.Sequence["PlacedOrder"]: int_free_slot_bits = int(free_slot_bits) int_is_bid_bits = int(is_bid_bits) placed_orders: typing.List[PlacedOrder] = [] @@ -46,7 +51,9 @@ class PlacedOrder: order_id = int(order_ids[index]) client_id = int(client_order_ids[index]) side = Side.BUY if int_is_bid_bits & (1 << index) else Side.SELL - placed_orders += [PlacedOrder(id=order_id, client_id=client_id, side=side)] + placed_orders += [ + PlacedOrder(id=order_id, client_id=client_id, side=side) + ] return placed_orders def __repr__(self) -> str: diff --git a/mango/publickey.py b/mango/publickey.py index 24984b9..c7fceec 100644 --- a/mango/publickey.py +++ b/mango/publickey.py @@ -39,5 +39,5 @@ def encode_public_key_for_sorting(address: PublicKey) -> typing.List[int]: int.from_bytes(raw[0:8], "big"), int.from_bytes(raw[8:16], "big"), int.from_bytes(raw[16:24], "big"), - int.from_bytes(raw[24:32], "big") + int.from_bytes(raw[24:32], "big"), ] diff --git a/mango/reconnectingwebsocket.py b/mango/reconnectingwebsocket.py index f37fd7f..f40a658 100644 --- a/mango/reconnectingwebsocket.py +++ b/mango/reconnectingwebsocket.py @@ -32,22 +32,26 @@ from threading import Thread # will continue to automatically reconnect, until it is explicitly closed. # class ReconnectingWebsocket: - def __init__(self, - url: str, - on_open_call: typing.Callable[[websocket.WebSocketApp], None]) -> None: + def __init__( + self, url: str, on_open_call: typing.Callable[[websocket.WebSocketApp], None] + ) -> None: self._logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.url = url self.on_open_call = on_open_call self.reconnect_required: bool = True self.ping_interval: int = 0 - self.connecting: rx.subject.behaviorsubject.BehaviorSubject = rx.subject.behaviorsubject.BehaviorSubject( - datetime.now()) - self.disconnected: rx.subject.behaviorsubject.BehaviorSubject = rx.subject.behaviorsubject.BehaviorSubject( - datetime.now()) - self.ping: rx.subject.behaviorsubject.BehaviorSubject = rx.subject.behaviorsubject.BehaviorSubject( - datetime.now()) - self.pong: rx.subject.behaviorsubject.BehaviorSubject = rx.subject.behaviorsubject.BehaviorSubject( - datetime.now()) + self.connecting: rx.subject.behaviorsubject.BehaviorSubject = ( + rx.subject.behaviorsubject.BehaviorSubject(datetime.now()) + ) + self.disconnected: rx.subject.behaviorsubject.BehaviorSubject = ( + rx.subject.behaviorsubject.BehaviorSubject(datetime.now()) + ) + self.ping: rx.subject.behaviorsubject.BehaviorSubject = ( + rx.subject.behaviorsubject.BehaviorSubject(datetime.now()) + ) + self.pong: rx.subject.behaviorsubject.BehaviorSubject = ( + rx.subject.behaviorsubject.BehaviorSubject(datetime.now()) + ) self.item: rx.subject.subject.Subject = rx.subject.subject.Subject() def close(self) -> None: @@ -94,7 +98,7 @@ class ReconnectingWebsocket: on_message=self._on_message, on_error=self._on_error, on_ping=self._on_ping, - on_pong=self._on_pong + on_pong=self._on_pong, ) self._ws.run_forever(ping_interval=self.ping_interval) self.disconnected.on_next(datetime.now()) diff --git a/mango/retrier.py b/mango/retrier.py index c5c571f..9741fb8 100644 --- a/mango/retrier.py +++ b/mango/retrier.py @@ -40,7 +40,12 @@ from decimal import Decimal # This class is best used in a `with...` block using the `retry_context()` function below. # class RetryWithPauses: - def __init__(self, name: str, func: typing.Callable[..., typing.Any], pauses: typing.Sequence[Decimal]) -> None: + def __init__( + self, + name: str, + func: typing.Callable[..., typing.Any], + pauses: typing.Sequence[Decimal], + ) -> None: self._logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.name: str = name self.func: typing.Callable[..., typing.Any] = func @@ -58,27 +63,40 @@ class RetryWithPauses: # or 413 for too much bandwidth." if exception.response.status_code == 413: self._logger.info( - f"Retriable call [{self.name}] rate limited (too much bandwidth) with error '{exception}'.") + f"Retriable call [{self.name}] rate limited (too much bandwidth) with error '{exception}'." + ) elif exception.response.status_code == 429: self._logger.info( - f"Retriable call [{self.name}] rate limited (too many requests) with error '{exception}'.") + f"Retriable call [{self.name}] rate limited (too many requests) with error '{exception}'." + ) else: self._logger.info( - f"Retriable call [{self.name}] failed with unexpected HTTP error '{exception}'.") + f"Retriable call [{self.name}] failed with unexpected HTTP error '{exception}'." + ) else: - self._logger.info(f"Retriable call [{self.name}] failed with unknown HTTP error '{exception}'.") + self._logger.info( + f"Retriable call [{self.name}] failed with unknown HTTP error '{exception}'." + ) except Exception as exception: - self._logger.info(f"Retriable call failed [{self.name}] with error '{exception}'.") + self._logger.info( + f"Retriable call failed [{self.name}] with error '{exception}'." + ) captured_exception = exception if sleep_time_on_error < 0: - self._logger.info(f"No more retries for [{self.name}] - propagating exception.") + self._logger.info( + f"No more retries for [{self.name}] - propagating exception." + ) raise captured_exception - self._logger.info(f"Will retry [{self.name}] call in {sleep_time_on_error} second(s).") + self._logger.info( + f"Will retry [{self.name}] call in {sleep_time_on_error} second(s)." + ) time.sleep(float(sleep_time_on_error)) - self._logger.info(f"End of retry loop for [{self.name}] - propagating exception.") + self._logger.info( + f"End of retry loop for [{self.name}] - propagating exception." + ) raise captured_exception @@ -95,6 +113,9 @@ class RetryWithPauses: # result = retrier.run(param1, param2) # ``` + @contextmanager -def retry_context(name: str, func: typing.Callable[..., typing.Any], pauses: typing.Sequence[Decimal]) -> typing.Iterator[RetryWithPauses]: +def retry_context( + name: str, func: typing.Callable[..., typing.Any], pauses: typing.Sequence[Decimal] +) -> typing.Iterator[RetryWithPauses]: yield RetryWithPauses(name, func, pauses) diff --git a/mango/serumeventqueue.py b/mango/serumeventqueue.py index 08d7a4d..d228338 100644 --- a/mango/serumeventqueue.py +++ b/mango/serumeventqueue.py @@ -31,7 +31,9 @@ from .version import Version # `SerumEventFlags` stores flags describing a `SerumEvent`. # class SerumEventFlags: - def __init__(self, version: Version, fill: bool, out: bool, bid: bool, maker: bool) -> None: + def __init__( + self, version: Version, fill: bool, out: bool, bid: bool, maker: bool + ) -> None: self.version: Version = version self.fill: bool = fill self.out: bool = out @@ -40,7 +42,13 @@ class SerumEventFlags: @staticmethod def from_layout(layout: typing.Any) -> "SerumEventFlags": - return SerumEventFlags(Version.UNSPECIFIED, bool(layout.fill), bool(layout.out), bool(layout.bid), bool(layout.maker)) + return SerumEventFlags( + Version.UNSPECIFIED, + bool(layout.fill), + bool(layout.out), + bool(layout.bid), + bool(layout.maker), + ) def __str__(self) -> str: flags: typing.List[typing.Optional[str]] = [] @@ -60,9 +68,19 @@ class SerumEventFlags: # `SerumEvent` stores details of an actual event in Serum. # class SerumEvent: - def __init__(self, version: Version, event_flags: SerumEventFlags, open_order_slot: Decimal, fee_tier: Decimal, - native_quantity_released: Decimal, native_quantity_paid: Decimal, native_fee_or_rebate: Decimal, - order_id: Decimal, public_key: PublicKey, client_order_id: Decimal) -> None: + def __init__( + self, + version: Version, + event_flags: SerumEventFlags, + open_order_slot: Decimal, + fee_tier: Decimal, + native_quantity_released: Decimal, + native_quantity_paid: Decimal, + native_fee_or_rebate: Decimal, + order_id: Decimal, + public_key: PublicKey, + client_order_id: Decimal, + ) -> None: self.version: Version = version self.event_flags: SerumEventFlags = event_flags self.open_order_slot: Decimal = open_order_slot @@ -78,9 +96,18 @@ class SerumEvent: @staticmethod def from_layout(layout: typing.Any) -> "SerumEvent": event_flags: SerumEventFlags = SerumEventFlags.from_layout(layout.event_flags) - return SerumEvent(Version.UNSPECIFIED, event_flags, layout.open_order_slot, layout.fee_tier, - layout.native_quantity_released, layout.native_quantity_paid, layout.native_fee_or_rebate, - layout.order_id, layout.public_key, layout.client_order_id) + return SerumEvent( + Version.UNSPECIFIED, + event_flags, + layout.open_order_slot, + layout.fee_tier, + layout.native_quantity_released, + layout.native_quantity_paid, + layout.native_fee_or_rebate, + layout.order_id, + layout.public_key, + layout.client_order_id, + ) def __str__(self) -> str: return f"""ยซ SerumEvent {self.event_flags} @@ -110,10 +137,17 @@ class SerumEvent: # market is new and there hasn't been a trade on it yet. # class SerumEventQueue(AddressableAccount): - def __init__(self, account_info: AccountInfo, version: Version, account_flags: AccountFlags, - head: Decimal, count: Decimal, sequence_number: Decimal, - unprocessed_events: typing.Sequence[SerumEvent], - processed_events: typing.Sequence[SerumEvent]) -> None: + def __init__( + self, + account_info: AccountInfo, + version: Version, + account_flags: AccountFlags, + head: Decimal, + count: Decimal, + sequence_number: Decimal, + unprocessed_events: typing.Sequence[SerumEvent], + processed_events: typing.Sequence[SerumEvent], + ) -> None: super().__init__(account_info) self.version: Version = version @@ -125,35 +159,50 @@ class SerumEventQueue(AddressableAccount): self.processed_events: typing.Sequence[SerumEvent] = processed_events @staticmethod - def from_layout(layout: typing.Any, account_info: AccountInfo, version: Version) -> "SerumEventQueue": + def from_layout( + layout: typing.Any, account_info: AccountInfo, version: Version + ) -> "SerumEventQueue": account_flags: AccountFlags = AccountFlags.from_layout(layout.account_flags) head: Decimal = layout.head count: Decimal = layout.count seq_num: Decimal = layout.next_seq_num events: typing.List[SerumEvent] = list( - map(SerumEvent.from_layout, [evt for evt in layout.events if evt is not None])) + map( + SerumEvent.from_layout, + [evt for evt in layout.events if evt is not None], + ) + ) for index, event in enumerate(events): event.original_index = Decimal(index) # Events are stored in a ringbuffer, and the oldest is overwritten when a new event arrives. # Make it a bit simpler to use by splitting at the insertion point and swapping the two pieces # around so that users don't have to do modulo arithmetic on the capacity. - ordered_events = events[int(head):] + events[0:int(head)] + ordered_events = events[int(head) :] + events[0 : int(head)] # Now chop the oldest-to-newest list of events into processed and unprocessed. The `count` # property holds the number of unprocessed events. - unprocessed_events = ordered_events[0:int(count)] - processed_events = ordered_events[int(count):] + unprocessed_events = ordered_events[0 : int(count)] + processed_events = ordered_events[int(count) :] - return SerumEventQueue(account_info, version, account_flags, head, count, seq_num, unprocessed_events, processed_events) + return SerumEventQueue( + account_info, + version, + account_flags, + head, + count, + seq_num, + unprocessed_events, + processed_events, + ) - @ staticmethod + @staticmethod def parse(account_info: AccountInfo) -> "SerumEventQueue": # Data length isn't fixed so can't check we get the right value the way we normally do. layout = layouts.SERUM_EVENT_QUEUE.parse(account_info.data) return SerumEventQueue.from_layout(layout, account_info, Version.V1) - @ staticmethod + @staticmethod def load(context: Context, address: PublicKey) -> "SerumEventQueue": account_info = AccountInfo.load(context, address) if account_info is None: @@ -181,10 +230,26 @@ class SerumEventQueue(AddressableAccount): return len(self.unprocessed_events) + len(self.processed_events) def __str__(self) -> str: - unprocessed_events = "\n ".join([f"{event}".replace("\n", "\n ") - for event in self.unprocessed_events if event is not None]) or "None" - processed_events = "\n ".join([f"{event}".replace("\n", "\n ") - for event in self.processed_events if event is not None]) or "None" + unprocessed_events = ( + "\n ".join( + [ + f"{event}".replace("\n", "\n ") + for event in self.unprocessed_events + if event is not None + ] + ) + or "None" + ) + processed_events = ( + "\n ".join( + [ + f"{event}".replace("\n", "\n ") + for event in self.processed_events + if event is not None + ] + ) + or "None" + ) return f"""ยซ SerumEventQueue [{self.version}] {self.address} {self.account_flags} Head: {self.head} @@ -217,7 +282,9 @@ class UnseenSerumEventChangesTracker: new_sequence_number: Decimal = event_queue.sequence_number if self.last_sequence_number != new_sequence_number: number_of_changes: Decimal = new_sequence_number - self.last_sequence_number - unseen = [*event_queue.processed_events, *event_queue.unprocessed_events][0 - int(number_of_changes):] + unseen = [*event_queue.processed_events, *event_queue.unprocessed_events][ + 0 - int(number_of_changes) : + ] self.last_sequence_number = new_sequence_number return unseen diff --git a/mango/serummarket.py b/mango/serummarket.py index 1e49211..553b6b9 100644 --- a/mango/serummarket.py +++ b/mango/serummarket.py @@ -36,15 +36,32 @@ from .token import Token # This class encapsulates our knowledge of a Serum spot market. # class SerumMarket(LoadedMarket): - def __init__(self, serum_program_address: PublicKey, address: PublicKey, base: Token, quote: Token, - underlying_serum_market: PySerumMarket) -> None: - super().__init__(serum_program_address, address, InventorySource.SPL_TOKENS, base, quote, RaisingLotSizeConverter()) + def __init__( + self, + serum_program_address: PublicKey, + address: PublicKey, + base: Token, + quote: Token, + underlying_serum_market: PySerumMarket, + ) -> None: + super().__init__( + serum_program_address, + address, + InventorySource.SPL_TOKENS, + base, + quote, + RaisingLotSizeConverter(), + ) self.base: Token = base self.quote: Token = quote self.underlying_serum_market: PySerumMarket = underlying_serum_market base_lot_size: Decimal = Decimal(underlying_serum_market.state.base_lot_size()) - quote_lot_size: Decimal = Decimal(underlying_serum_market.state.quote_lot_size()) - self.lot_size_converter: LotSizeConverter = LotSizeConverter(base, base_lot_size, quote, quote_lot_size) + quote_lot_size: Decimal = Decimal( + underlying_serum_market.state.quote_lot_size() + ) + self.lot_size_converter: LotSizeConverter = LotSizeConverter( + base, base_lot_size, quote, quote_lot_size + ) @property def bids_address(self) -> PublicKey: @@ -58,17 +75,31 @@ class SerumMarket(LoadedMarket): def event_queue_address(self) -> PublicKey: return self.underlying_serum_market.state.event_queue() - def parse_account_info_to_orders(self, account_info: AccountInfo) -> typing.Sequence[Order]: - orderbook: PySerumOrderBook = PySerumOrderBook.from_bytes(self.underlying_serum_market.state, account_info.data) + def parse_account_info_to_orders( + self, account_info: AccountInfo + ) -> typing.Sequence[Order]: + orderbook: PySerumOrderBook = PySerumOrderBook.from_bytes( + self.underlying_serum_market.state, account_info.data + ) return list(map(Order.from_serum_order, orderbook.orders())) def unprocessed_events(self, context: Context) -> typing.Sequence[SerumEvent]: - event_queue: SerumEventQueue = SerumEventQueue.load(context, self.event_queue_address) + event_queue: SerumEventQueue = SerumEventQueue.load( + context, self.event_queue_address + ) return event_queue.unprocessed_events - def find_openorders_address_for_owner(self, context: Context, owner: PublicKey) -> typing.Optional[PublicKey]: + def find_openorders_address_for_owner( + self, context: Context, owner: PublicKey + ) -> typing.Optional[PublicKey]: all_open_orders = OpenOrders.load_for_market_and_owner( - context, self.address, owner, context.serum_program_address, self.base.decimals, self.quote.decimals) + context, + self.address, + owner, + context.serum_program_address, + self.base.decimals, + self.quote.decimals, + ) if len(all_open_orders) == 0: return None return all_open_orders[0].address @@ -89,15 +120,39 @@ class SerumMarket(LoadedMarket): # This class holds information to load a `SerumMarket` object but doesn't automatically load it. # class SerumMarketStub(Market): - def __init__(self, serum_program_address: PublicKey, address: PublicKey, base: Token, quote: Token) -> None: - super().__init__(serum_program_address, address, InventorySource.SPL_TOKENS, base, quote, RaisingLotSizeConverter()) + def __init__( + self, + serum_program_address: PublicKey, + address: PublicKey, + base: Token, + quote: Token, + ) -> None: + super().__init__( + serum_program_address, + address, + InventorySource.SPL_TOKENS, + base, + quote, + RaisingLotSizeConverter(), + ) self.base: Token = base self.quote: Token = quote def load(self, context: Context) -> SerumMarket: underlying_serum_market: PySerumMarket = PySerumMarket.load( - context.client.compatible_client, self.address, context.serum_program_address) - return SerumMarket(self.program_address, self.address, self.base, self.quote, underlying_serum_market) + context.client.compatible_client, + self.address, + context.serum_program_address, + ) + return SerumMarket( + self.program_address, + self.address, + self.base, + self.quote, + underlying_serum_market, + ) def __str__(self) -> str: - return f"ยซ SerumMarketStub {self.symbol} {self.address} [{self.program_address}] ยป" + return ( + f"ยซ SerumMarketStub {self.symbol} {self.address} [{self.program_address}] ยป" + ) diff --git a/mango/serummarketlookup.py b/mango/serummarketlookup.py index f699231..daefb6d 100644 --- a/mango/serummarketlookup.py +++ b/mango/serummarketlookup.py @@ -50,31 +50,44 @@ from .token import Instrument, Token # there is a name-value pair for the particular market we're interested in. Also, the # current file only lists USDC and USDT markets, so that's all we can support this way. class SerumMarketLookup(MarketLookup): - def __init__(self, serum_program_address: PublicKey, token_data: typing.Dict[str, typing.Any]) -> None: + def __init__( + self, serum_program_address: PublicKey, token_data: typing.Dict[str, typing.Any] + ) -> None: super().__init__() self.serum_program_address: PublicKey = serum_program_address self.token_data: typing.Dict[str, typing.Any] = token_data @staticmethod - def load(serum_program_address: PublicKey, token_data_filename: str) -> "SerumMarketLookup": + def load( + serum_program_address: PublicKey, token_data_filename: str + ) -> "SerumMarketLookup": with open(token_data_filename, encoding="utf-8") as json_file: token_data = json.load(json_file) return SerumMarketLookup(serum_program_address, token_data) @staticmethod - def _find_data_by_symbol(symbol: str, token_data: typing.Dict[str, typing.Any]) -> typing.Optional[typing.Dict[str, typing.Any]]: + def _find_data_by_symbol( + symbol: str, token_data: typing.Dict[str, typing.Any] + ) -> typing.Optional[typing.Dict[str, typing.Any]]: for token in token_data["tokens"]: if Instrument.symbols_match(token["symbol"], symbol): return typing.cast(typing.Dict[str, typing.Any], token) return None @staticmethod - def _find_token_by_symbol_or_error(symbol: str, token_data: typing.Dict[str, typing.Any]) -> Token: + def _find_token_by_symbol_or_error( + symbol: str, token_data: typing.Dict[str, typing.Any] + ) -> Token: found_token_data = SerumMarketLookup._find_data_by_symbol(symbol, token_data) if found_token_data is None: raise Exception(f"Could not find data for token symbol '{symbol}'.") - return Token(symbol, found_token_data["name"], Decimal(found_token_data["decimals"]), PublicKey(found_token_data["address"])) + return Token( + symbol, + found_token_data["name"], + Decimal(found_token_data["decimals"]), + PublicKey(found_token_data["address"]), + ) def find_by_symbol(self, symbol: str) -> typing.Optional[Market]: if "/" not in symbol: @@ -89,15 +102,27 @@ class SerumMarketLookup(MarketLookup): if base_data is None: self._logger.warning(f"Could not find data for base token '{base_symbol}'") return None - base = Token(base_data["symbol"], base_data["name"], Decimal( - base_data["decimals"]), PublicKey(base_data["address"])) + base = Token( + base_data["symbol"], + base_data["name"], + Decimal(base_data["decimals"]), + PublicKey(base_data["address"]), + ) - quote_data = SerumMarketLookup._find_data_by_symbol(quote_symbol, self.token_data) + quote_data = SerumMarketLookup._find_data_by_symbol( + quote_symbol, self.token_data + ) if quote_data is None: - self._logger.warning(f"Could not find data for quote token '{quote_symbol}'") + self._logger.warning( + f"Could not find data for quote token '{quote_symbol}'" + ) return None - quote = Token(quote_data["symbol"], quote_data["name"], Decimal( - quote_data["decimals"]), PublicKey(quote_data["address"])) + quote = Token( + quote_data["symbol"], + quote_data["name"], + Decimal(quote_data["decimals"]), + PublicKey(quote_data["address"]), + ) if "extensions" not in base_data: self._logger.warning(f"No markets found for base token '{base.symbol}'.") @@ -105,21 +130,26 @@ class SerumMarketLookup(MarketLookup): if quote.symbol == "USDC": if "serumV3Usdc" not in base_data["extensions"]: - self._logger.warning(f"No USDC market found for base token '{base.symbol}'.") + self._logger.warning( + f"No USDC market found for base token '{base.symbol}'." + ) return None market_address_string = base_data["extensions"]["serumV3Usdc"] market_address = PublicKey(market_address_string) elif quote.symbol == "USDT": if "serumV3Usdt" not in base_data["extensions"]: - self._logger.warning(f"No USDT market found for base token '{base.symbol}'.") + self._logger.warning( + f"No USDT market found for base token '{base.symbol}'." + ) return None market_address_string = base_data["extensions"]["serumV3Usdt"] market_address = PublicKey(market_address_string) else: self._logger.warning( - f"Could not find market with quote token '{quote.symbol}'. Only markets based on USDC or USDT are supported.") + f"Could not find market with quote token '{quote.symbol}'. Only markets based on USDC or USDT are supported." + ) return None return SerumMarketStub(self.serum_program_address, market_address, base, quote) @@ -132,26 +162,54 @@ class SerumMarketLookup(MarketLookup): if token_data["extensions"]["serumV3Usdc"] == address_string: market_address_string = token_data["extensions"]["serumV3Usdc"] market_address = PublicKey(market_address_string) - base = Token(token_data["symbol"], token_data["name"], Decimal( - token_data["decimals"]), PublicKey(token_data["address"])) - quote_data = SerumMarketLookup._find_data_by_symbol("USDC", self.token_data) + base = Token( + token_data["symbol"], + token_data["name"], + Decimal(token_data["decimals"]), + PublicKey(token_data["address"]), + ) + quote_data = SerumMarketLookup._find_data_by_symbol( + "USDC", self.token_data + ) if quote_data is None: - raise Exception("Could not load token data for USDC (which should always be present).") - quote = Token(quote_data["symbol"], quote_data["name"], Decimal( - quote_data["decimals"]), PublicKey(quote_data["address"])) - return SerumMarketStub(self.serum_program_address, market_address, base, quote) + raise Exception( + "Could not load token data for USDC (which should always be present)." + ) + quote = Token( + quote_data["symbol"], + quote_data["name"], + Decimal(quote_data["decimals"]), + PublicKey(quote_data["address"]), + ) + return SerumMarketStub( + self.serum_program_address, market_address, base, quote + ) if "serumV3Usdt" in token_data["extensions"]: if token_data["extensions"]["serumV3Usdt"] == address_string: market_address_string = token_data["extensions"]["serumV3Usdt"] market_address = PublicKey(market_address_string) - base = Token(token_data["symbol"], token_data["name"], Decimal( - token_data["decimals"]), PublicKey(token_data["address"])) - quote_data = SerumMarketLookup._find_data_by_symbol("USDT", self.token_data) + base = Token( + token_data["symbol"], + token_data["name"], + Decimal(token_data["decimals"]), + PublicKey(token_data["address"]), + ) + quote_data = SerumMarketLookup._find_data_by_symbol( + "USDT", self.token_data + ) if quote_data is None: - raise Exception("Could not load token data for USDT (which should always be present).") - quote = Token(quote_data["symbol"], quote_data["name"], Decimal( - quote_data["decimals"]), PublicKey(quote_data["address"])) - return SerumMarketStub(self.serum_program_address, market_address, base, quote) + raise Exception( + "Could not load token data for USDT (which should always be present)." + ) + quote = Token( + quote_data["symbol"], + quote_data["name"], + Decimal(quote_data["decimals"]), + PublicKey(quote_data["address"]), + ) + return SerumMarketStub( + self.serum_program_address, market_address, base, quote + ) return None def all_markets(self) -> typing.Sequence[Market]: @@ -164,14 +222,30 @@ class SerumMarketLookup(MarketLookup): if "serumV3Usdc" in token_data["extensions"]: market_address_string = token_data["extensions"]["serumV3Usdc"] market_address = PublicKey(market_address_string) - base = Token(token_data["symbol"], token_data["name"], Decimal( - token_data["decimals"]), PublicKey(token_data["address"])) - all_markets += [SerumMarketStub(self.serum_program_address, market_address, base, usdc)] + base = Token( + token_data["symbol"], + token_data["name"], + Decimal(token_data["decimals"]), + PublicKey(token_data["address"]), + ) + all_markets += [ + SerumMarketStub( + self.serum_program_address, market_address, base, usdc + ) + ] if "serumV3Usdt" in token_data["extensions"]: market_address_string = token_data["extensions"]["serumV3Usdt"] market_address = PublicKey(market_address_string) - base = Token(token_data["symbol"], token_data["name"], Decimal( - token_data["decimals"]), PublicKey(token_data["address"])) - all_markets += [SerumMarketStub(self.serum_program_address, market_address, base, usdt)] + base = Token( + token_data["symbol"], + token_data["name"], + Decimal(token_data["decimals"]), + PublicKey(token_data["address"]), + ) + all_markets += [ + SerumMarketStub( + self.serum_program_address, market_address, base, usdt + ) + ] return all_markets diff --git a/mango/serummarketoperations.py b/mango/serummarketoperations.py index 6505839..86addbd 100644 --- a/mango/serummarketoperations.py +++ b/mango/serummarketoperations.py @@ -23,7 +23,12 @@ from solana.publickey import PublicKey from .combinableinstructions import CombinableInstructions from .constants import SYSTEM_PROGRAM_ADDRESS from .context import Context -from .instructions import build_create_serum_open_orders_instructions, build_serum_consume_events_instructions, build_serum_settle_instructions, build_serum_place_order_instructions +from .instructions import ( + build_create_serum_open_orders_instructions, + build_serum_consume_events_instructions, + build_serum_settle_instructions, + build_serum_place_order_instructions, +) from .marketoperations import MarketInstructionBuilder, MarketOperations from .openorders import OpenOrders from .orders import Order, OrderBook, Side @@ -43,10 +48,17 @@ from .wallet import Wallet # on initial setup in the `load()` method. # class SerumMarketInstructionBuilder(MarketInstructionBuilder): - def __init__(self, context: Context, wallet: Wallet, serum_market: SerumMarket, raw_market: PySerumMarket, - base_token_account: TokenAccount, quote_token_account: TokenAccount, - open_orders_address: typing.Optional[PublicKey], - fee_discount_token_address: PublicKey) -> None: + def __init__( + self, + context: Context, + wallet: Wallet, + serum_market: SerumMarket, + raw_market: PySerumMarket, + base_token_account: TokenAccount, + quote_token_account: TokenAccount, + open_orders_address: typing.Optional[PublicKey], + fee_discount_token_address: PublicKey, + ) -> None: super().__init__() self.context: Context = context self.wallet: Wallet = wallet @@ -58,40 +70,74 @@ class SerumMarketInstructionBuilder(MarketInstructionBuilder): self.fee_discount_token_address: PublicKey = fee_discount_token_address @staticmethod - def load(context: Context, wallet: Wallet, serum_market: SerumMarket) -> "SerumMarketInstructionBuilder": + def load( + context: Context, wallet: Wallet, serum_market: SerumMarket + ) -> "SerumMarketInstructionBuilder": raw_market: PySerumMarket = PySerumMarket.load( - context.client.compatible_client, serum_market.address, context.serum_program_address) + context.client.compatible_client, + serum_market.address, + context.serum_program_address, + ) fee_discount_token_address: PublicKey = SYSTEM_PROGRAM_ADDRESS - srm_instrument: typing.Optional[Instrument] = context.instrument_lookup.find_by_symbol("SRM") + srm_instrument: typing.Optional[ + Instrument + ] = context.instrument_lookup.find_by_symbol("SRM") if srm_instrument is not None: srm_token: Token = Token.ensure(srm_instrument) fee_discount_token_account = TokenAccount.fetch_largest_for_owner_and_token( - context, wallet.address, srm_token) + context, wallet.address, srm_token + ) if fee_discount_token_account is not None: fee_discount_token_address = fee_discount_token_account.address open_orders_address: typing.Optional[PublicKey] = None all_open_orders = OpenOrders.load_for_market_and_owner( - context, serum_market.address, wallet.address, context.serum_program_address, serum_market.base.decimals, serum_market.quote.decimals) + context, + serum_market.address, + wallet.address, + context.serum_program_address, + serum_market.base.decimals, + serum_market.quote.decimals, + ) if len(all_open_orders) > 0: open_orders_address = all_open_orders[0].address - base_token_account = TokenAccount.fetch_largest_for_owner_and_token(context, wallet.address, serum_market.base) + base_token_account = TokenAccount.fetch_largest_for_owner_and_token( + context, wallet.address, serum_market.base + ) if base_token_account is None: - raise Exception(f"Could not find source token account for base token {serum_market.base.symbol}.") + raise Exception( + f"Could not find source token account for base token {serum_market.base.symbol}." + ) quote_token_account = TokenAccount.fetch_largest_for_owner_and_token( - context, wallet.address, serum_market.quote) + context, wallet.address, serum_market.quote + ) if quote_token_account is None: - raise Exception(f"Could not find source token account for quote token {serum_market.quote.symbol}.") + raise Exception( + f"Could not find source token account for quote token {serum_market.quote.symbol}." + ) - return SerumMarketInstructionBuilder(context, wallet, serum_market, raw_market, base_token_account, quote_token_account, open_orders_address, fee_discount_token_address) + return SerumMarketInstructionBuilder( + context, + wallet, + serum_market, + raw_market, + base_token_account, + quote_token_account, + open_orders_address, + fee_discount_token_address, + ) - def build_cancel_order_instructions(self, order: Order, ok_if_missing: bool = False) -> CombinableInstructions: + def build_cancel_order_instructions( + self, order: Order, ok_if_missing: bool = False + ) -> CombinableInstructions: # For us to cancel an order, an open_orders account must already exist (or have existed). if self.open_orders_address is None: - raise Exception(f"Cannot cancel order with client ID {order.client_id} - no OpenOrders account.") + raise Exception( + f"Cannot cancel order with client ID {order.client_id} - no OpenOrders account." + ) raw_instruction = self.raw_market.make_cancel_order_by_client_id_instruction( self.wallet.keypair, self.open_orders_address, order.client_id @@ -106,23 +152,46 @@ class SerumMarketInstructionBuilder(MarketInstructionBuilder): if self.open_orders_address is None: raise Exception("Failed to find or create OpenOrders address") - payer_token_account = self.quote_token_account if order.side == Side.BUY else self.base_token_account - place = build_serum_place_order_instructions(self.context, self.wallet, self.raw_market, - payer_token_account.address, self.open_orders_address, - order.order_type, order.side, order.price, - order.quantity, order.client_id, - self.fee_discount_token_address) + payer_token_account = ( + self.quote_token_account + if order.side == Side.BUY + else self.base_token_account + ) + place = build_serum_place_order_instructions( + self.context, + self.wallet, + self.raw_market, + payer_token_account.address, + self.open_orders_address, + order.order_type, + order.side, + order.price, + order.quantity, + order.client_id, + self.fee_discount_token_address, + ) return ensure_open_orders + place def build_settle_instructions(self) -> CombinableInstructions: if self.open_orders_address is None: return CombinableInstructions.empty() - return build_serum_settle_instructions(self.context, self.wallet, self.raw_market, self.open_orders_address, self.base_token_account.address, self.quote_token_account.address) + return build_serum_settle_instructions( + self.context, + self.wallet, + self.raw_market, + self.open_orders_address, + self.base_token_account.address, + self.quote_token_account.address, + ) - def build_crank_instructions(self, addresses: typing.Sequence[PublicKey], limit: Decimal = Decimal(32)) -> CombinableInstructions: + def build_crank_instructions( + self, addresses: typing.Sequence[PublicKey], limit: Decimal = Decimal(32) + ) -> CombinableInstructions: if self.open_orders_address is None: - self._logger.debug("Returning empty crank instructions - no serum OpenOrders address provided.") + self._logger.debug( + "Returning empty crank instructions - no serum OpenOrders address provided." + ) return CombinableInstructions.empty() distinct_addresses: typing.List[PublicKey] = [self.open_orders_address] @@ -132,16 +201,29 @@ class SerumMarketInstructionBuilder(MarketInstructionBuilder): if len(distinct_addresses) > limit: self._logger.warn( - f"Cranking limited to {limit} of {len(distinct_addresses)} addresses waiting to be cranked.") + f"Cranking limited to {limit} of {len(distinct_addresses)} addresses waiting to be cranked." + ) - limited_addresses = distinct_addresses[0:min(int(limit), len(distinct_addresses))] + limited_addresses = distinct_addresses[ + 0 : min(int(limit), len(distinct_addresses)) + ] limited_addresses.sort(key=encode_public_key_for_sorting) - self._logger.debug(f"About to crank {len(limited_addresses)} addresses: {limited_addresses}") - return build_serum_consume_events_instructions(self.context, self.serum_market.address, self.raw_market.state.event_queue(), limited_addresses, int(limit)) + self._logger.debug( + f"About to crank {len(limited_addresses)} addresses: {limited_addresses}" + ) + return build_serum_consume_events_instructions( + self.context, + self.serum_market.address, + self.raw_market.state.event_queue(), + limited_addresses, + int(limit), + ) def build_create_openorders_instructions(self) -> CombinableInstructions: - create_open_orders = build_create_serum_open_orders_instructions(self.context, self.wallet, self.raw_market) + create_open_orders = build_create_serum_open_orders_instructions( + self.context, self.wallet, self.raw_market + ) self.open_orders_address = create_open_orders.signers[0].public_key return create_open_orders @@ -157,58 +239,101 @@ class SerumMarketInstructionBuilder(MarketInstructionBuilder): # This class performs standard operations on the Serum orderbook. # class SerumMarketOperations(MarketOperations): - def __init__(self, context: Context, wallet: Wallet, - market_instruction_builder: SerumMarketInstructionBuilder) -> None: + def __init__( + self, + context: Context, + wallet: Wallet, + market_instruction_builder: SerumMarketInstructionBuilder, + ) -> None: super().__init__(market_instruction_builder.serum_market) self.context: Context = context self.wallet: Wallet = wallet - self.market_instruction_builder: SerumMarketInstructionBuilder = market_instruction_builder + self.market_instruction_builder: SerumMarketInstructionBuilder = ( + market_instruction_builder + ) @property def serum_market(self) -> SerumMarket: return self.market_instruction_builder.serum_market - def cancel_order(self, order: Order, ok_if_missing: bool = False) -> typing.Sequence[str]: + def cancel_order( + self, order: Order, ok_if_missing: bool = False + ) -> typing.Sequence[str]: self._logger.info(f"Cancelling {self.serum_market.symbol} order {order}.") - signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) - cancel: CombinableInstructions = self.market_instruction_builder.build_cancel_order_instructions( - order, ok_if_missing=ok_if_missing) + signers: CombinableInstructions = CombinableInstructions.from_wallet( + self.wallet + ) + cancel: CombinableInstructions = ( + self.market_instruction_builder.build_cancel_order_instructions( + order, ok_if_missing=ok_if_missing + ) + ) crank: CombinableInstructions = self._build_crank() - settle: CombinableInstructions = self.market_instruction_builder.build_settle_instructions() + settle: CombinableInstructions = ( + self.market_instruction_builder.build_settle_instructions() + ) return (signers + cancel + crank + settle).execute(self.context) def place_order(self, order: Order, crank_limit: Decimal = Decimal(5)) -> Order: client_id: int = self.context.generate_client_id() - signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) + signers: CombinableInstructions = CombinableInstructions.from_wallet( + self.wallet + ) if order.reduce_only: - self._logger.warning("Ignoring reduce_only flag on order because Serum doesn't support it.") - open_orders_address = self.market_instruction_builder.open_orders_address or SYSTEM_PROGRAM_ADDRESS - order_with_client_id: Order = Order(id=0, client_id=client_id, side=order.side, price=order.price, - quantity=order.quantity, owner=open_orders_address, - order_type=order.order_type) - self._logger.info(f"Placing {self.serum_market.symbol} order {order_with_client_id}.") - place: CombinableInstructions = self.market_instruction_builder.build_place_order_instructions( - order_with_client_id) + self._logger.warning( + "Ignoring reduce_only flag on order because Serum doesn't support it." + ) + open_orders_address = ( + self.market_instruction_builder.open_orders_address + or SYSTEM_PROGRAM_ADDRESS + ) + order_with_client_id: Order = Order( + id=0, + client_id=client_id, + side=order.side, + price=order.price, + quantity=order.quantity, + owner=open_orders_address, + order_type=order.order_type, + ) + self._logger.info( + f"Placing {self.serum_market.symbol} order {order_with_client_id}." + ) + place: CombinableInstructions = ( + self.market_instruction_builder.build_place_order_instructions( + order_with_client_id + ) + ) crank: CombinableInstructions = self._build_crank(crank_limit) - settle: CombinableInstructions = self.market_instruction_builder.build_settle_instructions() + settle: CombinableInstructions = ( + self.market_instruction_builder.build_settle_instructions() + ) (signers + place + crank + settle).execute(self.context) return order def settle(self) -> typing.Sequence[str]: - signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) + signers: CombinableInstructions = CombinableInstructions.from_wallet( + self.wallet + ) settle = self.market_instruction_builder.build_settle_instructions() return (signers + settle).execute(self.context) def crank(self, limit: Decimal = Decimal(32)) -> typing.Sequence[str]: - signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) + signers: CombinableInstructions = CombinableInstructions.from_wallet( + self.wallet + ) crank = self._build_crank(limit) return (signers + crank).execute(self.context) def create_openorders(self) -> PublicKey: - signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) - create_open_orders = self.market_instruction_builder.build_create_openorders_instructions() + signers: CombinableInstructions = CombinableInstructions.from_wallet( + self.wallet + ) + create_open_orders = ( + self.market_instruction_builder.build_create_openorders_instructions() + ) open_orders_address = create_open_orders.signers[0].public_key (signers + create_open_orders).execute(self.context) @@ -230,20 +355,27 @@ class SerumMarketOperations(MarketOperations): orderbook: OrderBook = self.load_orderbook() return orderbook.all_orders_for_owner(open_orders_address) - def _build_crank(self, limit: Decimal = Decimal(32), add_self: bool = False) -> CombinableInstructions: + def _build_crank( + self, limit: Decimal = Decimal(32), add_self: bool = False + ) -> CombinableInstructions: open_orders_to_crank: typing.List[PublicKey] = [] for event in self.serum_market.unprocessed_events(self.context): open_orders_to_crank += [event.public_key] if add_self and self.market_instruction_builder.open_orders_address is not None: - open_orders_to_crank += [self.market_instruction_builder.open_orders_address] + open_orders_to_crank += [ + self.market_instruction_builder.open_orders_address + ] if len(open_orders_to_crank) == 0: return CombinableInstructions.empty() self._logger.debug( - f"Building crank instruction with {len(open_orders_to_crank)} public keys, throttled to {limit}") - return self.market_instruction_builder.build_crank_instructions(open_orders_to_crank, limit) + f"Building crank instruction with {len(open_orders_to_crank)} public keys, throttled to {limit}" + ) + return self.market_instruction_builder.build_crank_instructions( + open_orders_to_crank, limit + ) def __str__(self) -> str: return f"""ยซ SerumMarketOperations [{self.serum_market.symbol}] ยป""" diff --git a/mango/simplemarketmaking/simplemarketmaker.py b/mango/simplemarketmaking/simplemarketmaker.py index a870cdc..b366fce 100644 --- a/mango/simplemarketmaking/simplemarketmaker.py +++ b/mango/simplemarketmaking/simplemarketmaker.py @@ -54,7 +54,18 @@ from pathlib import Path # * Place and Cancel instructions aren't batched into single transactions # class SimpleMarketMaker: - def __init__(self, context: mango.Context, wallet: mango.Wallet, market: mango.SerumMarket, market_operations: mango.MarketOperations, oracle: mango.Oracle, spread_ratio: Decimal, position_size_ratio: Decimal, existing_order_tolerance: Decimal, pause: timedelta) -> None: + def __init__( + self, + context: mango.Context, + wallet: mango.Wallet, + market: mango.SerumMarket, + market_operations: mango.MarketOperations, + oracle: mango.Oracle, + spread_ratio: Decimal, + position_size_ratio: Decimal, + existing_order_tolerance: Decimal, + pause: timedelta, + ) -> None: self._logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.context: mango.Context = context self.wallet: mango.Wallet = wallet @@ -84,31 +95,40 @@ class SimpleMarketMaker: # Calculate what we want the orders to be. bid, ask = self.calculate_order_prices(price) - buy_quantity, sell_quantity = self.calculate_order_quantities(price, inventory) + buy_quantity, sell_quantity = self.calculate_order_quantities( + price, inventory + ) current_orders = self.market_operations.load_my_orders() - buy_orders = [order for order in current_orders if order.side == mango.Side.BUY] + buy_orders = [ + order for order in current_orders if order.side == mango.Side.BUY + ] if self.orders_require_action(buy_orders, bid, buy_quantity): self._logger.info("Cancelling BUY orders.") for order in buy_orders: self.market_operations.cancel_order(order) buy_order: mango.Order = mango.Order.from_basic_info( - mango.Side.BUY, bid, buy_quantity, mango.OrderType.POST_ONLY) + mango.Side.BUY, bid, buy_quantity, mango.OrderType.POST_ONLY + ) self.market_operations.place_order(buy_order) - sell_orders = [order for order in current_orders if order.side == mango.Side.SELL] + sell_orders = [ + order for order in current_orders if order.side == mango.Side.SELL + ] if self.orders_require_action(sell_orders, ask, sell_quantity): self._logger.info("Cancelling SELL orders.") for order in sell_orders: self.market_operations.cancel_order(order) sell_order: mango.Order = mango.Order.from_basic_info( - mango.Side.SELL, ask, sell_quantity, mango.OrderType.POST_ONLY) + mango.Side.SELL, ask, sell_quantity, mango.OrderType.POST_ONLY + ) self.market_operations.place_order(sell_order) self.update_health_on_successful_iteration() except Exception as exception: self._logger.warning( - f"Pausing and continuing after problem running market-making iteration: {exception} - {traceback.format_exc()}") + f"Pausing and continuing after problem running market-making iteration: {exception} - {traceback.format_exc()}" + ) # Wait and hope for fills. self._logger.info(f"Pausing for {self.pause} seconds.") @@ -129,60 +149,100 @@ class SimpleMarketMaker: for order in orders: self.market_operations.cancel_order(order) - def fetch_inventory(self) -> typing.Sequence[typing.Optional[mango.InstrumentValue]]: + def fetch_inventory( + self, + ) -> typing.Sequence[typing.Optional[mango.InstrumentValue]]: if self.market.inventory_source == mango.InventorySource.SPL_TOKENS: base_account = mango.TokenAccount.fetch_largest_for_owner_and_token( - self.context, self.wallet.address, self.market.base) + self.context, self.wallet.address, self.market.base + ) if base_account is None: raise Exception( - f"Could not find token account owned by {self.wallet.address} for base token {self.market.base}.") + f"Could not find token account owned by {self.wallet.address} for base token {self.market.base}." + ) quote_account = mango.TokenAccount.fetch_largest_for_owner_and_token( - self.context, self.wallet.address, self.market.quote) + self.context, self.wallet.address, self.market.quote + ) if quote_account is None: raise Exception( - f"Could not find token account owned by {self.wallet.address} for quote token {self.market.quote}.") + f"Could not find token account owned by {self.wallet.address} for quote token {self.market.quote}." + ) return [base_account.value, quote_account.value] else: group = mango.Group.load(self.context) - accounts = mango.Account.load_all_for_owner(self.context, self.wallet.address, group) + accounts = mango.Account.load_all_for_owner( + self.context, self.wallet.address, group + ) if len(accounts) == 0: raise Exception("No Mango account found.") account = accounts[0] return account.net_values_by_index - def calculate_order_prices(self, price: mango.Price) -> typing.Tuple[Decimal, Decimal]: + def calculate_order_prices( + self, price: mango.Price + ) -> typing.Tuple[Decimal, Decimal]: bid = price.mid_price - (price.mid_price * self.spread_ratio) ask = price.mid_price + (price.mid_price * self.spread_ratio) return (bid, ask) - def calculate_order_quantities(self, price: mango.Price, inventory: typing.Sequence[typing.Optional[mango.InstrumentValue]]) -> typing.Tuple[Decimal, Decimal]: - base_tokens: typing.Optional[mango.InstrumentValue] = mango.InstrumentValue.find_by_token( - inventory, self.market.base) + def calculate_order_quantities( + self, + price: mango.Price, + inventory: typing.Sequence[typing.Optional[mango.InstrumentValue]], + ) -> typing.Tuple[Decimal, Decimal]: + base_tokens: typing.Optional[ + mango.InstrumentValue + ] = mango.InstrumentValue.find_by_token(inventory, self.market.base) if base_tokens is None: - raise Exception(f"Could not find market-maker base token {self.market.base.symbol} in inventory.") + raise Exception( + f"Could not find market-maker base token {self.market.base.symbol} in inventory." + ) - quote_tokens: typing.Optional[mango.InstrumentValue] = mango.InstrumentValue.find_by_token( - inventory, self.market.quote) + quote_tokens: typing.Optional[ + mango.InstrumentValue + ] = mango.InstrumentValue.find_by_token(inventory, self.market.quote) if quote_tokens is None: - raise Exception(f"Could not find market-maker quote token {self.market.quote.symbol} in inventory.") + raise Exception( + f"Could not find market-maker quote token {self.market.quote.symbol} in inventory." + ) buy_quantity = base_tokens.value * self.position_size_ratio - sell_quantity = (quote_tokens.value / price.mid_price) * self.position_size_ratio + sell_quantity = ( + quote_tokens.value / price.mid_price + ) * self.position_size_ratio return (buy_quantity, sell_quantity) - def orders_require_action(self, orders: typing.Sequence[mango.Order], price: Decimal, quantity: Decimal) -> bool: - def within_tolerance(target_value: Decimal, order_value: Decimal, tolerance: Decimal) -> bool: + def orders_require_action( + self, orders: typing.Sequence[mango.Order], price: Decimal, quantity: Decimal + ) -> bool: + def within_tolerance( + target_value: Decimal, order_value: Decimal, tolerance: Decimal + ) -> bool: tolerated = order_value * tolerance - return bool((order_value < (target_value + tolerated)) and (order_value > (target_value - tolerated))) - return len(orders) == 0 or not all([(within_tolerance(price, order.price, self.existing_order_tolerance)) and within_tolerance(quantity, order.quantity, self.existing_order_tolerance) for order in orders]) + return bool( + (order_value < (target_value + tolerated)) + and (order_value > (target_value - tolerated)) + ) + + return len(orders) == 0 or not all( + [ + (within_tolerance(price, order.price, self.existing_order_tolerance)) + and within_tolerance( + quantity, order.quantity, self.existing_order_tolerance + ) + for order in orders + ] + ) def update_health_on_successful_iteration(self) -> None: try: Path(self.health_filename).touch(mode=0o666, exist_ok=True) except Exception as exception: - self._logger.warning(f"Touching file '{self.health_filename}' raised exception: {exception}") + self._logger.warning( + f"Touching file '{self.health_filename}' raised exception: {exception}" + ) def __str__(self) -> str: return f"""ยซ SimpleMarketMaker for market '{self.market.symbol}' ยป""" diff --git a/mango/spotmarket.py b/mango/spotmarket.py index 84bf0f5..c29383e 100644 --- a/mango/spotmarket.py +++ b/mango/spotmarket.py @@ -37,16 +37,34 @@ from .token import Token # This class encapsulates our knowledge of a Serum spot market. # class SpotMarket(LoadedMarket): - def __init__(self, serum_program_address: PublicKey, address: PublicKey, base: Token, quote: Token, - group: Group, underlying_serum_market: PySerumMarket) -> None: - super().__init__(serum_program_address, address, InventorySource.ACCOUNT, base, quote, RaisingLotSizeConverter()) + def __init__( + self, + serum_program_address: PublicKey, + address: PublicKey, + base: Token, + quote: Token, + group: Group, + underlying_serum_market: PySerumMarket, + ) -> None: + super().__init__( + serum_program_address, + address, + InventorySource.ACCOUNT, + base, + quote, + RaisingLotSizeConverter(), + ) self.base: Token = base self.quote: Token = quote self.group: Group = group self.underlying_serum_market: PySerumMarket = underlying_serum_market base_lot_size: Decimal = Decimal(underlying_serum_market.state.base_lot_size()) - quote_lot_size: Decimal = Decimal(underlying_serum_market.state.quote_lot_size()) - self.lot_size_converter: LotSizeConverter = LotSizeConverter(base, base_lot_size, quote, quote_lot_size) + quote_lot_size: Decimal = Decimal( + underlying_serum_market.state.quote_lot_size() + ) + self.lot_size_converter: LotSizeConverter = LotSizeConverter( + base, base_lot_size, quote, quote_lot_size + ) @property def bids_address(self) -> PublicKey: @@ -60,23 +78,33 @@ class SpotMarket(LoadedMarket): def event_queue_address(self) -> PublicKey: return self.underlying_serum_market.state.event_queue() - def parse_account_info_to_orders(self, account_info: AccountInfo) -> typing.Sequence[Order]: - orderbook: PySerumOrderBook = PySerumOrderBook.from_bytes(self.underlying_serum_market.state, account_info.data) + def parse_account_info_to_orders( + self, account_info: AccountInfo + ) -> typing.Sequence[Order]: + orderbook: PySerumOrderBook = PySerumOrderBook.from_bytes( + self.underlying_serum_market.state, account_info.data + ) return list(map(Order.from_serum_order, orderbook.orders())) def unprocessed_events(self, context: Context) -> typing.Sequence[SerumEvent]: - event_queue: SerumEventQueue = SerumEventQueue.load(context, self.event_queue_address) + event_queue: SerumEventQueue = SerumEventQueue.load( + context, self.event_queue_address + ) return event_queue.unprocessed_events - def derive_open_orders_address(self, context: Context, account: Account) -> PublicKey: + def derive_open_orders_address( + self, context: Context, account: Account + ) -> PublicKey: slot = account.slot_by_instrument(self.base) - open_orders_address_and_nonce: typing.Tuple[PublicKey, int] = PublicKey.find_program_address( + open_orders_address_and_nonce: typing.Tuple[ + PublicKey, int + ] = PublicKey.find_program_address( [ bytes(account.address), int(slot.index).to_bytes(8, "little"), - b"OpenOrders" + b"OpenOrders", ], - context.mango_program_address + context.mango_program_address, ) open_orders_address: PublicKey = open_orders_address_and_nonce[0] return open_orders_address @@ -97,9 +125,22 @@ class SpotMarket(LoadedMarket): # This class holds information to load a `SpotMarket` object but doesn't automatically load it. # class SpotMarketStub(Market): - def __init__(self, serum_program_address: PublicKey, address: PublicKey, base: Token, quote: Token, - group_address: PublicKey) -> None: - super().__init__(serum_program_address, address, InventorySource.ACCOUNT, base, quote, RaisingLotSizeConverter()) + def __init__( + self, + serum_program_address: PublicKey, + address: PublicKey, + base: Token, + quote: Token, + group_address: PublicKey, + ) -> None: + super().__init__( + serum_program_address, + address, + InventorySource.ACCOUNT, + base, + quote, + RaisingLotSizeConverter(), + ) self.base: Token = base self.quote: Token = quote self.group_address: PublicKey = group_address @@ -107,8 +148,20 @@ class SpotMarketStub(Market): def load(self, context: Context, group: typing.Optional[Group]) -> SpotMarket: actual_group: Group = group or Group.load(context, self.group_address) underlying_serum_market: PySerumMarket = PySerumMarket.load( - context.client.compatible_client, self.address, context.serum_program_address) - return SpotMarket(self.program_address, self.address, self.base, self.quote, actual_group, underlying_serum_market) + context.client.compatible_client, + self.address, + context.serum_program_address, + ) + return SpotMarket( + self.program_address, + self.address, + self.base, + self.quote, + actual_group, + underlying_serum_market, + ) def __str__(self) -> str: - return f"ยซ SpotMarketStub {self.symbol} {self.address} [{self.program_address}] ยป" + return ( + f"ยซ SpotMarketStub {self.symbol} {self.address} [{self.program_address}] ยป" + ) diff --git a/mango/spotmarketoperations.py b/mango/spotmarketoperations.py index 1ee13a3..b609626 100644 --- a/mango/spotmarketoperations.py +++ b/mango/spotmarketoperations.py @@ -25,7 +25,13 @@ from .combinableinstructions import CombinableInstructions from .constants import SYSTEM_PROGRAM_ADDRESS from .context import Context from .group import GroupSlot, Group -from .instructions import build_serum_consume_events_instructions, build_spot_place_order_instructions, build_cancel_spot_order_instructions, build_spot_settle_instructions, build_spot_openorders_instructions +from .instructions import ( + build_serum_consume_events_instructions, + build_spot_place_order_instructions, + build_cancel_spot_order_instructions, + build_spot_settle_instructions, + build_spot_openorders_instructions, +) from .marketoperations import MarketInstructionBuilder, MarketOperations from .orders import Order, OrderBook from .publickey import encode_public_key_for_sorting @@ -42,9 +48,17 @@ from .wallet import Wallet # on initial setup in the `load()` method. # class SpotMarketInstructionBuilder(MarketInstructionBuilder): - def __init__(self, context: Context, wallet: Wallet, spot_market: SpotMarket, - group: Group, account: Account, raw_market: PySerumMarket, - market_index: int, fee_discount_token_address: PublicKey) -> None: + def __init__( + self, + context: Context, + wallet: Wallet, + spot_market: SpotMarket, + group: Group, + account: Account, + raw_market: PySerumMarket, + market_index: int, + fee_discount_token_address: PublicKey, + ) -> None: super().__init__() self.context: Context = context self.wallet: Wallet = wallet @@ -55,41 +69,81 @@ class SpotMarketInstructionBuilder(MarketInstructionBuilder): self.market_index: int = market_index self.fee_discount_token_address: PublicKey = fee_discount_token_address - self.open_orders_address: typing.Optional[PublicKey] = self.account.spot_open_orders_by_index[self.market_index] + self.open_orders_address: typing.Optional[ + PublicKey + ] = self.account.spot_open_orders_by_index[self.market_index] @staticmethod - def load(context: Context, wallet: Wallet, spot_market: SpotMarket, group: Group, account: Account) -> "SpotMarketInstructionBuilder": + def load( + context: Context, + wallet: Wallet, + spot_market: SpotMarket, + group: Group, + account: Account, + ) -> "SpotMarketInstructionBuilder": raw_market: PySerumMarket = PySerumMarket.load( - context.client.compatible_client, spot_market.address, context.serum_program_address) + context.client.compatible_client, + spot_market.address, + context.serum_program_address, + ) msrm_balance = context.client.get_token_account_balance(group.msrm_vault) fee_discount_token_address: PublicKey if msrm_balance > 0: fee_discount_token_address = group.msrm_vault logging.debug( - f"MSRM balance is: {msrm_balance} - using MSRM fee discount address {fee_discount_token_address}") + f"MSRM balance is: {msrm_balance} - using MSRM fee discount address {fee_discount_token_address}" + ) else: fee_discount_token_address = group.srm_vault logging.debug( - f"MSRM balance is: {msrm_balance} - using SRM fee discount address {fee_discount_token_address}") + f"MSRM balance is: {msrm_balance} - using SRM fee discount address {fee_discount_token_address}" + ) slot = group.slot_by_spot_market_address(spot_market.address) market_index = slot.index - return SpotMarketInstructionBuilder(context, wallet, spot_market, group, account, raw_market, market_index, fee_discount_token_address) + return SpotMarketInstructionBuilder( + context, + wallet, + spot_market, + group, + account, + raw_market, + market_index, + fee_discount_token_address, + ) - def build_cancel_order_instructions(self, order: Order, ok_if_missing: bool = False) -> CombinableInstructions: + def build_cancel_order_instructions( + self, order: Order, ok_if_missing: bool = False + ) -> CombinableInstructions: if self.open_orders_address is None: return CombinableInstructions.empty() return build_cancel_spot_order_instructions( - self.context, self.wallet, self.group, self.account, self.raw_market, order, self.open_orders_address) + self.context, + self.wallet, + self.group, + self.account, + self.raw_market, + order, + self.open_orders_address, + ) def build_place_order_instructions(self, order: Order) -> CombinableInstructions: - return build_spot_place_order_instructions(self.context, self.wallet, self.group, self.account, - self.spot_market, order.order_type, order.side, order.price, - order.quantity, order.client_id, - self.fee_discount_token_address) + return build_spot_place_order_instructions( + self.context, + self.wallet, + self.group, + self.account, + self.spot_market, + order.order_type, + order.side, + order.price, + order.quantity, + order.client_id, + self.fee_discount_token_address, + ) def build_settle_instructions(self) -> CombinableInstructions: if self.open_orders_address is None: @@ -98,19 +152,33 @@ class SpotMarketInstructionBuilder(MarketInstructionBuilder): base_slot: GroupSlot = self.group.slot_by_instrument(self.spot_market.base) if base_slot.base_token_bank is None: raise Exception( - f"No token info for base instrument {self.spot_market.base.symbol} in group {self.group.address}") + f"No token info for base instrument {self.spot_market.base.symbol} in group {self.group.address}" + ) base_rootbank = base_slot.base_token_bank.ensure_root_bank(self.context) base_nodebank = base_rootbank.pick_node_bank(self.context) quote_rootbank = self.group.shared_quote.ensure_root_bank(self.context) quote_nodebank = quote_rootbank.pick_node_bank(self.context) - return build_spot_settle_instructions(self.context, self.wallet, self.account, - self.raw_market, self.group, self.open_orders_address, - base_rootbank, base_nodebank, quote_rootbank, quote_nodebank) + return build_spot_settle_instructions( + self.context, + self.wallet, + self.account, + self.raw_market, + self.group, + self.open_orders_address, + base_rootbank, + base_nodebank, + quote_rootbank, + quote_nodebank, + ) - def build_crank_instructions(self, addresses: typing.Sequence[PublicKey], limit: Decimal = Decimal(32)) -> CombinableInstructions: + def build_crank_instructions( + self, addresses: typing.Sequence[PublicKey], limit: Decimal = Decimal(32) + ) -> CombinableInstructions: if self.open_orders_address is None: - self._logger.debug("Returning empty crank instructions - no spot OpenOrders address provided.") + self._logger.debug( + "Returning empty crank instructions - no spot OpenOrders address provided." + ) return CombinableInstructions.empty() distinct_addresses: typing.List[PublicKey] = [self.open_orders_address] @@ -120,19 +188,32 @@ class SpotMarketInstructionBuilder(MarketInstructionBuilder): if len(distinct_addresses) > limit: self._logger.warn( - f"Cranking limited to {limit} of {len(distinct_addresses)} addresses waiting to be cranked.") + f"Cranking limited to {limit} of {len(distinct_addresses)} addresses waiting to be cranked." + ) - limited_addresses = distinct_addresses[0:min(int(limit), len(distinct_addresses))] + limited_addresses = distinct_addresses[ + 0 : min(int(limit), len(distinct_addresses)) + ] limited_addresses.sort(key=encode_public_key_for_sorting) - self._logger.debug(f"About to crank {len(limited_addresses)} addresses: {limited_addresses}") - return build_serum_consume_events_instructions(self.context, self.spot_market.address, self.raw_market.state.event_queue(), limited_addresses, int(limit)) + self._logger.debug( + f"About to crank {len(limited_addresses)} addresses: {limited_addresses}" + ) + return build_serum_consume_events_instructions( + self.context, + self.spot_market.address, + self.raw_market.state.event_queue(), + limited_addresses, + int(limit), + ) def build_redeem_instructions(self) -> CombinableInstructions: return CombinableInstructions.empty() def build_create_openorders_instructions(self) -> CombinableInstructions: - return build_spot_openorders_instructions(self.context, self.wallet, self.group, self.account, self.spot_market) + return build_spot_openorders_instructions( + self.context, self.wallet, self.group, self.account, self.spot_market + ) def __str__(self) -> str: return f"ยซ SpotMarketInstructionBuilder [{self.spot_market.symbol}] ยป" @@ -143,16 +224,27 @@ class SpotMarketInstructionBuilder(MarketInstructionBuilder): # This class puts trades on the Serum orderbook. It doesn't do anything complicated. # class SpotMarketOperations(MarketOperations): - def __init__(self, context: Context, wallet: Wallet, account: Account, - market_instruction_builder: SpotMarketInstructionBuilder) -> None: + def __init__( + self, + context: Context, + wallet: Wallet, + account: Account, + market_instruction_builder: SpotMarketInstructionBuilder, + ) -> None: super().__init__(market_instruction_builder.spot_market) self.context: Context = context self.wallet: Wallet = wallet self.account: Account = account - self.market_instruction_builder: SpotMarketInstructionBuilder = market_instruction_builder + self.market_instruction_builder: SpotMarketInstructionBuilder = ( + market_instruction_builder + ) - self.market_index: int = self.group.slot_by_spot_market_address(self.spot_market.address).index - self.open_orders_address: typing.Optional[PublicKey] = self.account.spot_open_orders_by_index[self.market_index] + self.market_index: int = self.group.slot_by_spot_market_address( + self.spot_market.address + ).index + self.open_orders_address: typing.Optional[ + PublicKey + ] = self.account.spot_open_orders_by_index[self.market_index] @property def spot_market(self) -> SpotMarket: @@ -162,28 +254,49 @@ class SpotMarketOperations(MarketOperations): def group(self) -> Group: return self.market_instruction_builder.group - def cancel_order(self, order: Order, ok_if_missing: bool = False) -> typing.Sequence[str]: + def cancel_order( + self, order: Order, ok_if_missing: bool = False + ) -> typing.Sequence[str]: self._logger.info(f"Cancelling {self.spot_market.symbol} order {order}.") - signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) - cancel: CombinableInstructions = self.market_instruction_builder.build_cancel_order_instructions( - order, ok_if_missing=ok_if_missing) + signers: CombinableInstructions = CombinableInstructions.from_wallet( + self.wallet + ) + cancel: CombinableInstructions = ( + self.market_instruction_builder.build_cancel_order_instructions( + order, ok_if_missing=ok_if_missing + ) + ) crank: CombinableInstructions = self._build_crank(add_self=True) - settle: CombinableInstructions = self.market_instruction_builder.build_settle_instructions() + settle: CombinableInstructions = ( + self.market_instruction_builder.build_settle_instructions() + ) return (signers + cancel + crank + settle).execute(self.context) def place_order(self, order: Order, crank_limit: Decimal = Decimal(5)) -> Order: client_id: int = self.context.generate_client_id() - signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) + signers: CombinableInstructions = CombinableInstructions.from_wallet( + self.wallet + ) if order.reduce_only: - self._logger.warning("Ignoring reduce_only flag on order because spot markets don't support it.") + self._logger.warning( + "Ignoring reduce_only flag on order because spot markets don't support it." + ) order_with_client_id: Order = order.with_client_id(client_id).with_owner( - self.open_orders_address or SYSTEM_PROGRAM_ADDRESS) + self.open_orders_address or SYSTEM_PROGRAM_ADDRESS + ) self._logger.info(f"Placing {self.spot_market.symbol} order {order}.") - place: CombinableInstructions = self.market_instruction_builder.build_place_order_instructions( - order_with_client_id) - crank: CombinableInstructions = self._build_crank(limit=crank_limit, add_self=True) - settle: CombinableInstructions = self.market_instruction_builder.build_settle_instructions() + place: CombinableInstructions = ( + self.market_instruction_builder.build_place_order_instructions( + order_with_client_id + ) + ) + crank: CombinableInstructions = self._build_crank( + limit=crank_limit, add_self=True + ) + settle: CombinableInstructions = ( + self.market_instruction_builder.build_settle_instructions() + ) transaction_ids = (signers + place + crank + settle).execute(self.context) self._logger.info(f"Transaction IDs: {transaction_ids}.") @@ -191,30 +304,44 @@ class SpotMarketOperations(MarketOperations): return order_with_client_id def settle(self) -> typing.Sequence[str]: - signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) + signers: CombinableInstructions = CombinableInstructions.from_wallet( + self.wallet + ) settle = self.market_instruction_builder.build_settle_instructions() return (signers + settle).execute(self.context) def crank(self, limit: Decimal = Decimal(32)) -> typing.Sequence[str]: - signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) + signers: CombinableInstructions = CombinableInstructions.from_wallet( + self.wallet + ) crank = self._build_crank(limit, add_self=False) return (signers + crank).execute(self.context) def create_openorders(self) -> PublicKey: - signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) - create_open_orders: CombinableInstructions = self.market_instruction_builder.build_create_openorders_instructions() + signers: CombinableInstructions = CombinableInstructions.from_wallet( + self.wallet + ) + create_open_orders: CombinableInstructions = ( + self.market_instruction_builder.build_create_openorders_instructions() + ) (signers + create_open_orders).execute(self.context) # These lines are a little nasty. Now that we know we have an OpenOrders account at this address, # update the Account so that future uses (like later in this method) have access to it in the right # place. - open_orders_address: PublicKey = self.spot_market.derive_open_orders_address(self.context, self.account) - self.account.update_spot_open_orders_for_market(self.market_index, open_orders_address) + open_orders_address: PublicKey = self.spot_market.derive_open_orders_address( + self.context, self.account + ) + self.account.update_spot_open_orders_for_market( + self.market_index, open_orders_address + ) return open_orders_address def ensure_openorders(self) -> PublicKey: - existing: typing.Optional[PublicKey] = self.account.spot_open_orders_by_index[self.market_index] + existing: typing.Optional[PublicKey] = self.account.spot_open_orders_by_index[ + self.market_index + ] if existing is not None: return existing return self.create_openorders() @@ -229,7 +356,9 @@ class SpotMarketOperations(MarketOperations): orderbook: OrderBook = self.load_orderbook() return orderbook.all_orders_for_owner(self.open_orders_address) - def _build_crank(self, limit: Decimal = Decimal(32), add_self: bool = False) -> CombinableInstructions: + def _build_crank( + self, limit: Decimal = Decimal(32), add_self: bool = False + ) -> CombinableInstructions: open_orders_to_crank: typing.List[PublicKey] = [] for event in self.spot_market.unprocessed_events(self.context): open_orders_to_crank += [event.public_key] @@ -241,8 +370,11 @@ class SpotMarketOperations(MarketOperations): return CombinableInstructions.empty() self._logger.debug( - f"Building crank instruction with {len(open_orders_to_crank)} public keys, throttled to {limit}") - return self.market_instruction_builder.build_crank_instructions(open_orders_to_crank, limit) + f"Building crank instruction with {len(open_orders_to_crank)} public keys, throttled to {limit}" + ) + return self.market_instruction_builder.build_crank_instructions( + open_orders_to_crank, limit + ) def __str__(self) -> str: return f"ยซ SpotMarketOperations [{self.spot_market.symbol}] ยป" diff --git a/mango/token.py b/mango/token.py index 5465e5d..cb0812a 100644 --- a/mango/token.py +++ b/mango/token.py @@ -37,12 +37,12 @@ class Instrument: return round(value, int(self.decimals)) def shift_to_decimals(self, value: Decimal) -> Decimal: - divisor = Decimal(10 ** self.decimals) + divisor = Decimal(10**self.decimals) shifted = value / divisor return shifted def shift_to_native(self, value: Decimal) -> Decimal: - multiplier = Decimal(10 ** self.decimals) + multiplier = Decimal(10**self.decimals) shifted = value * multiplier return round(shifted, 0) @@ -67,14 +67,18 @@ class Instrument: # `Token` defines aspects common to every token. # class Token(Instrument): - def __init__(self, symbol: str, name: str, decimals: Decimal, mint: PublicKey) -> None: + def __init__( + self, symbol: str, name: str, decimals: Decimal, mint: PublicKey + ) -> None: super().__init__(symbol, name, decimals) self.mint: PublicKey = mint def ensure(uncertain_token: Instrument) -> "Token": if isinstance(uncertain_token, Token): return uncertain_token - raise Exception(f"Instrument {uncertain_token} cannot be converted to SPL Token") + raise Exception( + f"Instrument {uncertain_token} cannot be converted to SPL Token" + ) @staticmethod def find_by_symbol(values: typing.Sequence["Token"], symbol: str) -> "Token": @@ -83,7 +87,9 @@ class Token(Instrument): raise Exception(f"Token '{symbol}' not found in token values: {values}") if len(found) > 1: - raise Exception(f"Token '{symbol}' matched multiple tokens in values: {values}") + raise Exception( + f"Token '{symbol}' matched multiple tokens in values: {values}" + ) return found[0] @@ -94,7 +100,9 @@ class Token(Instrument): raise Exception(f"Token '{mint}' not found in token values: {values}") if len(found) > 1: - raise Exception(f"Token '{mint}' matched multiple tokens in values: {values}") + raise Exception( + f"Token '{mint}' matched multiple tokens in values: {values}" + ) return found[0] diff --git a/mango/tokenaccount.py b/mango/tokenaccount.py index c219a13..e120de7 100644 --- a/mango/tokenaccount.py +++ b/mango/tokenaccount.py @@ -38,7 +38,13 @@ from .wallet import Wallet # # ๐Ÿฅญ TokenAccount class # class TokenAccount(AddressableAccount): - def __init__(self, account_info: AccountInfo, version: Version, owner: PublicKey, value: InstrumentValue) -> None: + def __init__( + self, + account_info: AccountInfo, + version: Version, + owner: PublicKey, + value: InstrumentValue, + ) -> None: super().__init__(account_info) self.version: Version = version self.owner: PublicKey = owner @@ -46,47 +52,73 @@ class TokenAccount(AddressableAccount): @staticmethod def create(context: Context, account: Keypair, token: Token) -> "TokenAccount": - spl_token = SplToken(context.client.compatible_client, token.mint, TOKEN_PROGRAM_ID, account) + spl_token = SplToken( + context.client.compatible_client, token.mint, TOKEN_PROGRAM_ID, account + ) owner = account.public_key new_account_address = spl_token.create_account(owner) - created: typing.Optional[TokenAccount] = TokenAccount.load(context, new_account_address) + created: typing.Optional[TokenAccount] = TokenAccount.load( + context, new_account_address + ) if created is None: - raise Exception(f"Newly-created SPL token account could not be found at address {new_account_address}") + raise Exception( + f"Newly-created SPL token account could not be found at address {new_account_address}" + ) return created @staticmethod - def fetch_all_for_owner_and_token(context: Context, owner_public_key: PublicKey, token: Token) -> typing.Sequence["TokenAccount"]: + def fetch_all_for_owner_and_token( + context: Context, owner_public_key: PublicKey, token: Token + ) -> typing.Sequence["TokenAccount"]: opts = TokenAccountOpts(mint=token.mint) - token_accounts = context.client.get_token_accounts_by_owner(owner_public_key, opts) + token_accounts = context.client.get_token_accounts_by_owner( + owner_public_key, opts + ) all_accounts: typing.List[TokenAccount] = [] for token_account_response in token_accounts: account_info = AccountInfo._from_response_values( - token_account_response["account"], PublicKey(token_account_response["pubkey"])) + token_account_response["account"], + PublicKey(token_account_response["pubkey"]), + ) token_account = TokenAccount.parse(account_info, token) all_accounts += [token_account] return all_accounts @staticmethod - def fetch_largest_for_owner_and_token(context: Context, owner_public_key: PublicKey, token: Token) -> typing.Optional["TokenAccount"]: - all_accounts = TokenAccount.fetch_all_for_owner_and_token(context, owner_public_key, token) + def fetch_largest_for_owner_and_token( + context: Context, owner_public_key: PublicKey, token: Token + ) -> typing.Optional["TokenAccount"]: + all_accounts = TokenAccount.fetch_all_for_owner_and_token( + context, owner_public_key, token + ) largest_account: typing.Optional[TokenAccount] = None for token_account in all_accounts: - if largest_account is None or token_account.value.value > largest_account.value.value: + if ( + largest_account is None + or token_account.value.value > largest_account.value.value + ): largest_account = token_account return largest_account @staticmethod - def fetch_or_create_largest_for_owner_and_token(context: Context, account: Keypair, token: Token) -> "TokenAccount": - all_accounts = TokenAccount.fetch_all_for_owner_and_token(context, account.public_key, token) + def fetch_or_create_largest_for_owner_and_token( + context: Context, account: Keypair, token: Token + ) -> "TokenAccount": + all_accounts = TokenAccount.fetch_all_for_owner_and_token( + context, account.public_key, token + ) largest_account: typing.Optional[TokenAccount] = None for token_account in all_accounts: - if largest_account is None or token_account.value.value > largest_account.value.value: + if ( + largest_account is None + or token_account.value.value > largest_account.value.value + ): largest_account = token_account if largest_account is None: @@ -95,10 +127,16 @@ class TokenAccount(AddressableAccount): return largest_account @staticmethod - def find_or_create_token_address_to_use(context: Context, wallet: Wallet, owner: PublicKey, token: Token) -> PublicKey: + def find_or_create_token_address_to_use( + context: Context, wallet: Wallet, owner: PublicKey, token: Token + ) -> PublicKey: # This is a root wallet account - get the token account to use. - associated_token_address = spl_token.get_associated_token_address(owner, token.mint) - token_account: typing.Optional[TokenAccount] = TokenAccount.load(context, associated_token_address) + associated_token_address = spl_token.get_associated_token_address( + owner, token.mint + ) + token_account: typing.Optional[TokenAccount] = TokenAccount.load( + context, associated_token_address + ) if token_account is not None: # The associated token account exists so use it return associated_token_address @@ -111,7 +149,9 @@ class TokenAccount(AddressableAccount): # There is no old-style token account either, so create the proper associated token account. signer = CombinableInstructions.from_wallet(wallet) - create_instruction = spl_token.create_associated_token_account(wallet.address, owner, token.mint) + create_instruction = spl_token.create_associated_token_account( + wallet.address, owner, token.mint + ) create = CombinableInstructions.from_instruction(create_instruction) transaction_ids = (signer + create).execute(context) @@ -120,24 +160,39 @@ class TokenAccount(AddressableAccount): return associated_token_address @staticmethod - def from_layout(layout: typing.Any, account_info: AccountInfo, token: Token) -> "TokenAccount": + def from_layout( + layout: typing.Any, account_info: AccountInfo, token: Token + ) -> "TokenAccount": token_value = InstrumentValue(token, token.shift_to_decimals(layout.amount)) - return TokenAccount(account_info, Version.UNSPECIFIED, layout.owner, token_value) + return TokenAccount( + account_info, Version.UNSPECIFIED, layout.owner, token_value + ) @staticmethod - def parse(account_info: AccountInfo, token: typing.Optional[Token] = None, instrument_lookup: typing.Optional[InstrumentLookup] = None) -> "TokenAccount": + def parse( + account_info: AccountInfo, + token: typing.Optional[Token] = None, + instrument_lookup: typing.Optional[InstrumentLookup] = None, + ) -> "TokenAccount": data = account_info.data if len(data) != layouts.TOKEN_ACCOUNT.sizeof(): raise Exception( - f"Data length ({len(data)}) does not match expected size ({layouts.TOKEN_ACCOUNT.sizeof()})") + f"Data length ({len(data)}) does not match expected size ({layouts.TOKEN_ACCOUNT.sizeof()})" + ) layout = layouts.TOKEN_ACCOUNT.parse(data) if token is None: if instrument_lookup is None: - raise Exception("Neither 'Token' or 'InstrumentLookup' specified for parsing token data.") - instrument: typing.Optional[Instrument] = instrument_lookup.find_by_mint(layout.mint) + raise Exception( + "Neither 'Token' or 'InstrumentLookup' specified for parsing token data." + ) + instrument: typing.Optional[Instrument] = instrument_lookup.find_by_mint( + layout.mint + ) if instrument is None: - raise Exception(f"Could not find token data for token with mint '{layout.mint}'") + raise Exception( + f"Could not find token data for token with mint '{layout.mint}'" + ) token = Token.ensure(instrument) return TokenAccount.from_layout(layout, account_info, token) @@ -145,9 +200,15 @@ class TokenAccount(AddressableAccount): @staticmethod def load(context: Context, address: PublicKey) -> typing.Optional["TokenAccount"]: account_info = AccountInfo.load(context, address) - if account_info is None or (len(account_info.data) != layouts.TOKEN_ACCOUNT.sizeof()): + if account_info is None or ( + len(account_info.data) != layouts.TOKEN_ACCOUNT.sizeof() + ): return None - return TokenAccount.parse(account_info, instrument_lookup=context.instrument_lookup) + return TokenAccount.parse( + account_info, instrument_lookup=context.instrument_lookup + ) def __str__(self) -> str: - return f"ยซ TokenAccount {self.address}, Owner: {self.owner}, Value: {self.value} ยป" + return ( + f"ยซ TokenAccount {self.address}, Owner: {self.owner}, Value: {self.value} ยป" + ) diff --git a/mango/tokenbank.py b/mango/tokenbank.py index 0ab1810..39689e2 100644 --- a/mango/tokenbank.py +++ b/mango/tokenbank.py @@ -42,7 +42,9 @@ class InterestRates: borrow: Decimal def __str__(self) -> str: - return f"ยซ InterestRates Deposit: {self.deposit:,.2%} Borrow: {self.borrow:,.2%} ยป" + return ( + f"ยซ InterestRates Deposit: {self.deposit:,.2%} Borrow: {self.borrow:,.2%} ยป" + ) def __repr__(self) -> str: return f"{self}" @@ -69,8 +71,14 @@ class BankBalances: # `NodeBank` stores details of deposits/borrows and vault. # class NodeBank(AddressableAccount): - def __init__(self, account_info: AccountInfo, version: Version, meta_data: Metadata, - vault: PublicKey, balances: BankBalances) -> None: + def __init__( + self, + account_info: AccountInfo, + version: Version, + meta_data: Metadata, + vault: PublicKey, + balances: BankBalances, + ) -> None: super().__init__(account_info) self.version: Version = version self.meta_data: Metadata = meta_data @@ -78,7 +86,9 @@ class NodeBank(AddressableAccount): self.balances: BankBalances = balances @staticmethod - def from_layout(layout: typing.Any, account_info: AccountInfo, version: Version) -> "NodeBank": + def from_layout( + layout: typing.Any, account_info: AccountInfo, version: Version + ) -> "NodeBank": meta_data: Metadata = layout.meta_data deposits: Decimal = layout.deposits borrows: Decimal = layout.borrows @@ -92,7 +102,8 @@ class NodeBank(AddressableAccount): data = account_info.data if len(data) != layouts.NODE_BANK.sizeof(): raise Exception( - f"NodeBank data length ({len(data)}) does not match expected size ({layouts.NODE_BANK.sizeof()})") + f"NodeBank data length ({len(data)}) does not match expected size ({layouts.NODE_BANK.sizeof()})" + ) layout = layouts.NODE_BANK.parse(data) return NodeBank.from_layout(layout, account_info, Version.V1) @@ -120,10 +131,19 @@ class NodeBank(AddressableAccount): # `RootBank` stores details of how to reach `NodeBank`. # class RootBank(AddressableAccount): - def __init__(self, account_info: AccountInfo, version: Version, meta_data: Metadata, - optimal_util: Decimal, optimal_rate: Decimal, max_rate: Decimal, - node_banks: typing.Sequence[PublicKey], deposit_index: Decimal, - borrow_index: Decimal, last_updated: datetime) -> None: + def __init__( + self, + account_info: AccountInfo, + version: Version, + meta_data: Metadata, + optimal_util: Decimal, + optimal_rate: Decimal, + max_rate: Decimal, + node_banks: typing.Sequence[PublicKey], + deposit_index: Decimal, + borrow_index: Decimal, + last_updated: datetime, + ) -> None: super().__init__(account_info) self.version: Version = version @@ -142,7 +162,9 @@ class RootBank(AddressableAccount): def ensure_node_banks(self, context: Context) -> typing.Sequence[NodeBank]: if self.loaded_node_banks is None: - node_bank_account_infos = AccountInfo.load_multiple(context, self.node_banks) + node_bank_account_infos = AccountInfo.load_multiple( + context, self.node_banks + ) self.loaded_node_banks = list(map(NodeBank.parse, node_bank_account_infos)) return self.loaded_node_banks @@ -164,7 +186,9 @@ class RootBank(AddressableAccount): return BankBalances(deposits=total_deposits, borrows=total_borrows) @staticmethod - def from_layout(layout: typing.Any, account_info: AccountInfo, version: Version) -> "RootBank": + def from_layout( + layout: typing.Any, account_info: AccountInfo, version: Version + ) -> "RootBank": meta_data: Metadata = Metadata.from_layout(layout.meta_data) optimal_util: Decimal = layout.optimal_util @@ -172,19 +196,33 @@ class RootBank(AddressableAccount): max_rate: Decimal = layout.max_rate num_node_banks: Decimal = layout.num_node_banks - node_banks: typing.Sequence[PublicKey] = layout.node_banks[0:int(num_node_banks)] + node_banks: typing.Sequence[PublicKey] = layout.node_banks[ + 0 : int(num_node_banks) + ] deposit_index: Decimal = layout.deposit_index borrow_index: Decimal = layout.borrow_index last_updated: datetime = layout.last_updated - return RootBank(account_info, version, meta_data, optimal_util, optimal_rate, max_rate, node_banks, deposit_index, borrow_index, last_updated) + return RootBank( + account_info, + version, + meta_data, + optimal_util, + optimal_rate, + max_rate, + node_banks, + deposit_index, + borrow_index, + last_updated, + ) @staticmethod def parse(account_info: AccountInfo) -> "RootBank": data = account_info.data if len(data) != layouts.ROOT_BANK.sizeof(): raise Exception( - f"RootBank data length ({len(data)}) does not match expected size ({layouts.ROOT_BANK.sizeof()})") + f"RootBank data length ({len(data)}) does not match expected size ({layouts.ROOT_BANK.sizeof()})" + ) layout = layouts.ROOT_BANK.parse(data) return RootBank.from_layout(layout, account_info, Version.V1) @@ -197,7 +235,9 @@ class RootBank(AddressableAccount): return RootBank.parse(account_info) @staticmethod - def load_multiple(context: Context, addresses: typing.Sequence[PublicKey]) -> typing.Sequence["RootBank"]: + def load_multiple( + context: Context, addresses: typing.Sequence[PublicKey] + ) -> typing.Sequence["RootBank"]: account_infos = AccountInfo.load_multiple(context, addresses) root_banks = [] for account_info in account_infos: @@ -207,13 +247,17 @@ class RootBank(AddressableAccount): return root_banks @staticmethod - def find_by_address(values: typing.Sequence["RootBank"], address: PublicKey) -> "RootBank": + def find_by_address( + values: typing.Sequence["RootBank"], address: PublicKey + ) -> "RootBank": found = [value for value in values if value.address == address] if len(found) == 0: raise Exception(f"RootBank '{address}' not found in root banks: {values}") if len(found) > 1: - raise Exception(f"RootBank '{address}' matched multiple root banks in: {values}") + raise Exception( + f"RootBank '{address}' matched multiple root banks in: {values}" + ) return found[0] @@ -238,7 +282,7 @@ class RootBank(AddressableAccount): # # `TokenBank` defines additional information for a `Token`. # -class TokenBank(): +class TokenBank: def __init__(self, token: Token, root_bank_address: PublicKey) -> None: self._logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.token: Token = token @@ -247,11 +291,15 @@ class TokenBank(): self.loaded_root_bank: typing.Optional[RootBank] = None @staticmethod - def from_layout_or_none(layout: typing.Any, instrument_lookup: InstrumentLookup) -> typing.Optional["TokenBank"]: + def from_layout_or_none( + layout: typing.Any, instrument_lookup: InstrumentLookup + ) -> typing.Optional["TokenBank"]: if layout.mint is None: return None - instrument: typing.Optional[Instrument] = instrument_lookup.find_by_mint(layout.mint) + instrument: typing.Optional[Instrument] = instrument_lookup.find_by_mint( + layout.mint + ) if instrument is None: raise Exception(f"Token with mint {layout.mint} could not be found.") token: Token = Token.ensure(instrument) @@ -260,23 +308,35 @@ class TokenBank(): if decimals != token.decimals: raise Exception( - f"Conflict between number of decimals in token static data {token.decimals} and group {decimals} for token {token.symbol}.") + f"Conflict between number of decimals in token static data {token.decimals} and group {decimals} for token {token.symbol}." + ) return TokenBank(token, root_bank_address) @staticmethod - def find_by_symbol(values: typing.Sequence[typing.Optional["TokenBank"]], symbol: str) -> "TokenBank": + def find_by_symbol( + values: typing.Sequence[typing.Optional["TokenBank"]], symbol: str + ) -> "TokenBank": found = [ - value for value in values if value is not None and value.token is not None and value.token.symbol_matches(symbol)] + value + for value in values + if value is not None + and value.token is not None + and value.token.symbol_matches(symbol) + ] if len(found) == 0: raise Exception(f"Token '{symbol}' not found in token infos: {values}") if len(found) > 1: - raise Exception(f"Token '{symbol}' matched multiple tokens in infos: {values}") + raise Exception( + f"Token '{symbol}' matched multiple tokens in infos: {values}" + ) return found[0] - def root_bank_cache_from_cache(self, cache: Cache, index: int) -> typing.Optional[RootBankCache]: + def root_bank_cache_from_cache( + self, cache: Cache, index: int + ) -> typing.Optional[RootBankCache]: return cache.root_bank_cache[index] def ensure_root_bank(self, context: Context) -> RootBank: @@ -306,7 +366,9 @@ class TokenBank(): borrow_rate = slope * utilization else: extra_utilization = utilization - root_bank.optimal_util - slope = (root_bank.max_rate - root_bank.optimal_rate) / (1 - root_bank.optimal_util) + slope = (root_bank.max_rate - root_bank.optimal_rate) / ( + 1 - root_bank.optimal_util + ) borrow_rate = root_bank.optimal_rate + (slope * extra_utilization) if balances.deposits == 0: diff --git a/mango/tradeexecutor.py b/mango/tradeexecutor.py index 66efc3d..0a0edf2 100644 --- a/mango/tradeexecutor.py +++ b/mango/tradeexecutor.py @@ -59,11 +59,15 @@ class TradeExecutor(metaclass=abc.ABCMeta): @abc.abstractmethod def buy(self, symbol: str, quantity: Decimal) -> Order: - raise NotImplementedError("TradeExecutor.buy() is not implemented on the base type.") + raise NotImplementedError( + "TradeExecutor.buy() is not implemented on the base type." + ) @abc.abstractmethod def sell(self, symbol: str, quantity: Decimal) -> Order: - raise NotImplementedError("TradeExecutor.sell() is not implemented on the base type.") + raise NotImplementedError( + "TradeExecutor.sell() is not implemented on the base type." + ) def __repr__(self) -> str: return f"{self}" @@ -75,7 +79,9 @@ class TradeExecutor(metaclass=abc.ABCMeta): # is expected, but which will not actually trade. # class NullTradeExecutor(TradeExecutor): - def __init__(self, reporter: typing.Optional[typing.Callable[[str], None]] = None) -> None: + def __init__( + self, reporter: typing.Optional[typing.Callable[[str], None]] = None + ) -> None: super().__init__() self.reporter: typing.Callable[[str], None] = reporter or (lambda _: None) @@ -117,7 +123,14 @@ class NullTradeExecutor(TradeExecutor): # assuming) the price exceeded the price specified. # class ImmediateTradeExecutor(TradeExecutor): - def __init__(self, context: Context, wallet: Wallet, account: typing.Optional[Account], price_adjustment_factor: Decimal = Decimal(0), reporter: typing.Optional[typing.Callable[[str], None]] = None) -> None: + def __init__( + self, + context: Context, + wallet: Wallet, + account: typing.Optional[Account], + price_adjustment_factor: Decimal = Decimal(0), + reporter: typing.Optional[typing.Callable[[str], None]] = None, + ) -> None: super().__init__() self.context: Context = context self.wallet: Wallet = wallet @@ -130,6 +143,7 @@ class ImmediateTradeExecutor(TradeExecutor): self._logger.info(text) if reporter is not None: reporter(text) + self.reporter = _reporter def buy(self, symbol: str, quantity: Decimal) -> Order: @@ -142,7 +156,9 @@ class ImmediateTradeExecutor(TradeExecutor): increase_factor = Decimal(1) + self.price_adjustment_factor price = top_ask * increase_factor - self.reporter(f"Price {price} - adjusted by {self.price_adjustment_factor} from {top_ask}") + self.reporter( + f"Price {price} - adjusted by {self.price_adjustment_factor} from {top_ask}" + ) order = Order.from_basic_info(Side.BUY, price, quantity, OrderType.IOC) return market_operations.place_order(order) @@ -157,7 +173,9 @@ class ImmediateTradeExecutor(TradeExecutor): decrease_factor = Decimal(1) - self.price_adjustment_factor price = top_bid * decrease_factor - self.reporter(f"Price {price} - adjusted by {self.price_adjustment_factor} from {top_bid}") + self.reporter( + f"Price {price} - adjusted by {self.price_adjustment_factor} from {top_bid}" + ) order = Order.from_basic_info(Side.SELL, price, quantity, OrderType.IOC) return market_operations.place_order(order) diff --git a/mango/tradehistory.py b/mango/tradehistory.py index 7d08f8f..5b9c267 100644 --- a/mango/tradehistory.py +++ b/mango/tradehistory.py @@ -36,14 +36,26 @@ from .context import Context # Downloads and unifies trade history data. # class TradeHistory: - COLUMNS = ["Timestamp", "Market", "Side", "MakerOrTaker", "Change", "Price", "Quantity", "Fee", - "SequenceNumber", "FeeTier", "MarketType", "OrderId"] + COLUMNS = [ + "Timestamp", + "Market", + "Side", + "MakerOrTaker", + "Change", + "Price", + "Quantity", + "Fee", + "SequenceNumber", + "FeeTier", + "MarketType", + "OrderId", + ] __perp_column_name_mapper = { "loadTimestamp": "Timestamp", "seqNum": "SequenceNumber", "price": "Price", - "quantity": "Quantity" + "quantity": "Quantity", } __spot_column_name_mapper = { @@ -54,7 +66,7 @@ class TradeHistory: "side": "Side", "feeCost": "Fee", "feeTier": "FeeTier", - "orderId": "OrderId" + "orderId": "OrderId", } __decimal_spot_columns = [ @@ -71,7 +83,7 @@ class TradeHistory: "quoteTokenDecimals", "price", "feeCost", - "size" + "size", ] __decimal_perp_columns = [ @@ -81,7 +93,7 @@ class TradeHistory: "makerOrderId", "takerOrderId", "price", - "quantity" + "quantity", ] __column_converters = { @@ -92,7 +104,7 @@ class TradeHistory: "Quantity": lambda value: Decimal(value), "Fee": lambda value: Decimal(value), "FeeTier": lambda value: Decimal(value), - "OrderId": lambda value: Decimal(value) + "OrderId": lambda value: Decimal(value), } def __init__(self, seconds_pause_between_rest_calls: int = 1) -> None: @@ -108,6 +120,7 @@ class TradeHistory: if market is None: raise Exception(f"No market found with address {address}") return market.symbol + return __safe_lookup @staticmethod @@ -120,12 +133,19 @@ class TradeHistory: def __download_all_perps(context: Context, account: Account) -> pandas.DataFrame: url = f"https://event-history-api.herokuapp.com/perp_trades/{account.address}?page=all" data = TradeHistory.__download_json(url) - trades: pandas.DataFrame = TradeHistory.__perp_data_to_dataframe(context, account, data) + trades: pandas.DataFrame = TradeHistory.__perp_data_to_dataframe( + context, account, data + ) return trades @staticmethod - def __download_updated_perps(context: Context, account: Account, newer_than: typing.Optional[datetime], seconds_pause_between_rest_calls: int) -> pandas.DataFrame: + def __download_updated_perps( + context: Context, + account: Account, + newer_than: typing.Optional[datetime], + seconds_pause_between_rest_calls: int, + ) -> pandas.DataFrame: trades: pandas.DataFrame = pandas.DataFrame(columns=TradeHistory.COLUMNS) page: int = 0 complete: bool = False @@ -133,12 +153,16 @@ class TradeHistory: page += 1 url = f"https://event-history-api.herokuapp.com/perp_trades/{account.address}?page={page}" data = TradeHistory.__download_json(url) - frame: pandas.DataFrame = TradeHistory.__perp_data_to_dataframe(context, account, data) + frame: pandas.DataFrame = TradeHistory.__perp_data_to_dataframe( + context, account, data + ) if len(frame) == 0: complete = True else: trades = trades.append(frame) - if (newer_than is not None) and (frame.loc[frame.index[-1], "Timestamp"] < newer_than): + if (newer_than is not None) and ( + frame.loc[frame.index[-1], "Timestamp"] < newer_than + ): complete = True else: time.sleep(seconds_pause_between_rest_calls) @@ -146,7 +170,9 @@ class TradeHistory: return trades @staticmethod - def __perp_data_to_dataframe(context: Context, account: Account, data: typing.Any) -> pandas.DataFrame: + def __perp_data_to_dataframe( + context: Context, account: Account, data: typing.Any + ) -> pandas.DataFrame: # Perp data is an array of JSON packages like: # { # "loadTimestamp": "2021-09-02T10:54:56.000Z", @@ -188,21 +214,33 @@ class TradeHistory: for column_name in TradeHistory.__decimal_perp_columns: trade[column_name] = Decimal(trade[column_name]) - frame = pandas.DataFrame(trade_data).rename(mapper=TradeHistory.__perp_column_name_mapper, axis=1, copy=True) - frame["Timestamp"] = frame["Timestamp"].apply(lambda timestamp: parser.parse(timestamp).replace(microsecond=0)) + frame = pandas.DataFrame(trade_data).rename( + mapper=TradeHistory.__perp_column_name_mapper, axis=1, copy=True + ) + frame["Timestamp"] = frame["Timestamp"].apply( + lambda timestamp: parser.parse(timestamp).replace(microsecond=0) + ) frame["Market"] = frame.apply(TradeHistory.__market_lookup(context), axis=1) frame["MarketType"] = "perp" this_address = f"{account.address}" - frame["MakerOrTaker"] = frame["maker"].apply(lambda addy: "maker" if addy == this_address else "taker") + frame["MakerOrTaker"] = frame["maker"].apply( + lambda addy: "maker" if addy == this_address else "taker" + ) frame["FeeTier"] = -1 frame["Fee"] = frame.apply(__fee_calculator, axis=1) frame["Side"] = frame.apply(__side_lookup, axis=1) frame["Value"] = frame["Price"] * frame["Quantity"] - frame["Change"] = frame["Value"].where(frame["Side"] == "sell", other=-frame["Value"]) + frame["Fee"] - frame["OrderId"] = numpy.where(frame["MakerOrTaker"] == "maker", - frame["makerOrderId"], frame["takerOrderId"]) + frame["Change"] = ( + frame["Value"].where(frame["Side"] == "sell", other=-frame["Value"]) + + frame["Fee"] + ) + frame["OrderId"] = numpy.where( + frame["MakerOrTaker"] == "maker", + frame["makerOrderId"], + frame["takerOrderId"], + ) return frame[TradeHistory.COLUMNS] @@ -218,7 +256,12 @@ class TradeHistory: return trades @staticmethod - def __download_updated_spots(context: Context, account: Account, newer_than: typing.Optional[datetime], seconds_pause_between_rest_calls: int) -> pandas.DataFrame: + def __download_updated_spots( + context: Context, + account: Account, + newer_than: typing.Optional[datetime], + seconds_pause_between_rest_calls: int, + ) -> pandas.DataFrame: trades: pandas.DataFrame = pandas.DataFrame(columns=TradeHistory.COLUMNS) for spot_open_orders_address in account.spot_open_orders: page: int = 0 @@ -242,7 +285,9 @@ class TradeHistory: return trades @staticmethod - def __spot_data_to_dataframe(context: Context, account: Account, data: typing.Any) -> pandas.DataFrame: + def __spot_data_to_dataframe( + context: Context, account: Account, data: typing.Any + ) -> pandas.DataFrame: # Spot data is an array of JSON packages like: # { # "loadTimestamp": "2021-10-05T16:04:50.717Z", @@ -281,14 +326,19 @@ class TradeHistory: trade[column_name] = Decimal(trade[column_name]) frame = pandas.DataFrame(trade_data).rename( - mapper=TradeHistory.__spot_column_name_mapper, axis=1, copy=True) + mapper=TradeHistory.__spot_column_name_mapper, axis=1, copy=True + ) frame["Timestamp"] = frame["Timestamp"].apply( - lambda timestamp: parser.parse(timestamp).replace(microsecond=0)) + lambda timestamp: parser.parse(timestamp).replace(microsecond=0) + ) frame["Market"] = frame.apply(TradeHistory.__market_lookup(context), axis=1) frame["MakerOrTaker"] = numpy.where(frame["maker"], "maker", "taker") frame["Fee"] = -frame["Fee"] frame["Value"] = frame["Price"] * frame["Quantity"] - frame["Change"] = frame["Value"].where(frame["Side"] == "sell", other=-frame["Value"]) + frame["Fee"] + frame["Change"] = ( + frame["Value"].where(frame["Side"] == "sell", other=-frame["Value"]) + + frame["Fee"] + ) frame["MarketType"] = "spot" return frame[TradeHistory.COLUMNS] @@ -297,31 +347,38 @@ class TradeHistory: def trades(self) -> pandas.DataFrame: return self.__trades.copy(deep=True) - def download_latest(self, context: Context, account: Account, cutoff: datetime) -> None: + def download_latest( + self, context: Context, account: Account, cutoff: datetime + ) -> None: # Go back further than we need to so we can be sure we're not skipping any trades due to race conditions. # We remove duplicates a few lines further down. self._logger.info(f"Downloading spot trades from {cutoff}") - spot: pandas.DataFrame = TradeHistory.__download_updated_spots(context, - account, - cutoff, - self.__seconds_pause_between_rest_calls) + spot: pandas.DataFrame = TradeHistory.__download_updated_spots( + context, account, cutoff, self.__seconds_pause_between_rest_calls + ) self._logger.info(f"Downloading perp trades from {cutoff}") - perp: pandas.DataFrame = TradeHistory.__download_updated_perps(context, - account, - cutoff, - self.__seconds_pause_between_rest_calls) + perp: pandas.DataFrame = TradeHistory.__download_updated_perps( + context, account, cutoff, self.__seconds_pause_between_rest_calls + ) all_trades: pandas.DataFrame = pandas.concat([self.__trades, spot, perp]) all_trades = all_trades[all_trades["Timestamp"] >= cutoff] distinct_trades = all_trades.drop_duplicates() - sorted_trades = distinct_trades.sort_values(["Timestamp", "Market", "SequenceNumber"], axis=0, ascending=True) - self._logger.info(f"Download complete. Data contains {len(sorted_trades)} trades.") + sorted_trades = distinct_trades.sort_values( + ["Timestamp", "Market", "SequenceNumber"], axis=0, ascending=True + ) + self._logger.info( + f"Download complete. Data contains {len(sorted_trades)} trades." + ) self.__trades = sorted_trades def update(self, context: Context, account: Account) -> None: - latest_trade: typing.Optional[datetime] = self.__trades.loc[self.__trades.index[-1], - "Timestamp"] if len(self.__trades) > 0 else None + latest_trade: typing.Optional[datetime] = ( + self.__trades.loc[self.__trades.index[-1], "Timestamp"] + if len(self.__trades) > 0 + else None + ) spot: pandas.DataFrame perp: pandas.DataFrame if latest_trade is None: @@ -335,18 +392,26 @@ class TradeHistory: cutoff_safety_margin: timedelta = timedelta(hours=1) cutoff: datetime = latest_trade - cutoff_safety_margin self._logger.info( - f"Downloading spot trades from {cutoff}, {cutoff_safety_margin} before latest stored trade at {latest_trade}") - spot = TradeHistory.__download_updated_spots(context, account, - cutoff, self.__seconds_pause_between_rest_calls) + f"Downloading spot trades from {cutoff}, {cutoff_safety_margin} before latest stored trade at {latest_trade}" + ) + spot = TradeHistory.__download_updated_spots( + context, account, cutoff, self.__seconds_pause_between_rest_calls + ) self._logger.info( - f"Downloading perp trades from {cutoff}, {cutoff_safety_margin} before latest stored trade at {latest_trade}") - perp = TradeHistory.__download_updated_perps(context, account, - cutoff, self.__seconds_pause_between_rest_calls) + f"Downloading perp trades from {cutoff}, {cutoff_safety_margin} before latest stored trade at {latest_trade}" + ) + perp = TradeHistory.__download_updated_perps( + context, account, cutoff, self.__seconds_pause_between_rest_calls + ) all_trades = pandas.concat([self.__trades, spot, perp]) distinct_trades = all_trades.drop_duplicates() - sorted_trades = distinct_trades.sort_values(["Timestamp", "Market", "SequenceNumber"], axis=0, ascending=True) - self._logger.info(f"Download complete. Data contains {len(sorted_trades)} trades.") + sorted_trades = distinct_trades.sort_values( + ["Timestamp", "Market", "SequenceNumber"], axis=0, ascending=True + ) + self._logger.info( + f"Download complete. Data contains {len(sorted_trades)} trades." + ) self.__trades = sorted_trades def load(self, filename: str, ok_if_missing: bool = False) -> None: @@ -354,9 +419,11 @@ class TradeHistory: if not ok_if_missing: raise Exception(f"File {filename} does not exist or is not a file.") else: - existing = pandas.read_csv(filename, - float_precision="round_trip", - converters=TradeHistory.__column_converters) + existing = pandas.read_csv( + filename, + float_precision="round_trip", + converters=TradeHistory.__column_converters, + ) self.__trades = self.__trades.append(existing) diff --git a/mango/transactionscout.py b/mango/transactionscout.py index 5301669..9312b5c 100644 --- a/mango/transactionscout.py +++ b/mango/transactionscout.py @@ -65,11 +65,18 @@ from .ownedinstrumentvalue import OwnedInstrumentValue # # ๐Ÿฅญ TransactionScout class # class TransactionScout: - def __init__(self, timestamp: datetime.datetime, signatures: typing.Sequence[str], - succeeded: bool, group_name: str, accounts: typing.Sequence[PublicKey], - instructions: typing.Sequence[MangoInstruction], messages: typing.Sequence[str], - pre_token_balances: typing.Sequence[OwnedInstrumentValue], - post_token_balances: typing.Sequence[OwnedInstrumentValue]) -> None: + def __init__( + self, + timestamp: datetime.datetime, + signatures: typing.Sequence[str], + succeeded: bool, + group_name: str, + accounts: typing.Sequence[PublicKey], + instructions: typing.Sequence[MangoInstruction], + messages: typing.Sequence[str], + pre_token_balances: typing.Sequence[OwnedInstrumentValue], + post_token_balances: typing.Sequence[OwnedInstrumentValue], + ) -> None: self.timestamp: datetime.datetime = timestamp self.signatures: typing.Sequence[str] = signatures self.succeeded: bool = succeeded @@ -77,28 +84,47 @@ class TransactionScout: self.accounts: typing.Sequence[PublicKey] = accounts self.instructions: typing.Sequence[MangoInstruction] = instructions self.messages: typing.Sequence[str] = messages - self.pre_token_balances: typing.Sequence[OwnedInstrumentValue] = pre_token_balances - self.post_token_balances: typing.Sequence[OwnedInstrumentValue] = post_token_balances + self.pre_token_balances: typing.Sequence[ + OwnedInstrumentValue + ] = pre_token_balances + self.post_token_balances: typing.Sequence[ + OwnedInstrumentValue + ] = post_token_balances @property def summary(self) -> str: result = "[Success]" if self.succeeded else "[Failed]" - instructions = ", ".join([ins.instruction_type.name for ins in self.instructions]) - changes = OwnedInstrumentValue.changes(self.pre_token_balances, self.post_token_balances) + instructions = ", ".join( + [ins.instruction_type.name for ins in self.instructions] + ) + changes = OwnedInstrumentValue.changes( + self.pre_token_balances, self.post_token_balances + ) in_tokens = [] for ins in self.instructions: if ins.token_in_account is not None: - in_tokens += [OwnedInstrumentValue.find_by_owner(changes, ins.token_in_account)] + in_tokens += [ + OwnedInstrumentValue.find_by_owner(changes, ins.token_in_account) + ] out_tokens = [] for ins in self.instructions: if ins.token_out_account is not None: - out_tokens += [OwnedInstrumentValue.find_by_owner(changes, ins.token_out_account)] + out_tokens += [ + 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" + ) return f"ยซ TransactionScout {result} {self.group_name} [{self.timestamp}] {instructions}: Token Changes: {changed_tokens_text}\n {self.signatures} ยป" @@ -111,10 +137,14 @@ class TransactionScout: return self.instructions[0].group def has_any_instruction_of_type(self, instruction_type: InstructionType) -> bool: - return any(map(lambda ins: ins.instruction_type == instruction_type, self.instructions)) + return any( + map(lambda ins: ins.instruction_type == instruction_type, self.instructions) + ) @staticmethod - def load_if_available(context: Context, signature: str) -> typing.Optional["TransactionScout"]: + def load_if_available( + context: Context, signature: str + ) -> typing.Optional["TransactionScout"]: transaction_details = context.client.get_confirmed_transaction(signature) if transaction_details is None: return None @@ -128,8 +158,12 @@ class TransactionScout: return tx @staticmethod - def from_transaction_response(context: Context, response: typing.Dict[str, typing.Any]) -> "TransactionScout": - def balance_to_token_value(accounts: typing.Sequence[PublicKey], balance: typing.Dict[str, typing.Any]) -> OwnedInstrumentValue: + def from_transaction_response( + context: Context, response: typing.Dict[str, typing.Any] + ) -> "TransactionScout": + def balance_to_token_value( + accounts: typing.Sequence[PublicKey], balance: typing.Dict[str, typing.Any] + ) -> OwnedInstrumentValue: mint = PublicKey(balance["mint"]) account = accounts[balance["accountIndex"]] amount = Decimal(balance["uiTokenAmount"]["amount"]) @@ -141,10 +175,14 @@ class TransactionScout: try: succeeded = True if response["meta"]["err"] is None else False - accounts = list(map(PublicKey, response["transaction"]["message"]["accountKeys"])) + accounts = list( + map(PublicKey, response["transaction"]["message"]["accountKeys"]) + ) instructions: typing.List[MangoInstruction] = [] for instruction_data in response["transaction"]["message"]["instructions"]: - instruction = mango_instruction_from_response(context, accounts, instruction_data) + instruction = mango_instruction_from_response( + context, accounts, instruction_data + ) if instruction is not None: instructions += [instruction] @@ -152,37 +190,61 @@ class TransactionScout: timestamp = datetime.datetime.fromtimestamp(response["blockTime"]) signatures = response["transaction"]["signatures"] messages = response["meta"]["logMessages"] - pre_token_balances = list(map(lambda bal: balance_to_token_value( - accounts, bal), response["meta"]["preTokenBalances"])) - post_token_balances = list(map(lambda bal: balance_to_token_value( - accounts, bal), response["meta"]["postTokenBalances"])) - return TransactionScout(timestamp, - signatures, - succeeded, - group_name, - accounts, - instructions, - messages, - pre_token_balances, - post_token_balances) + pre_token_balances = list( + map( + lambda bal: balance_to_token_value(accounts, bal), + response["meta"]["preTokenBalances"], + ) + ) + post_token_balances = list( + map( + lambda bal: balance_to_token_value(accounts, bal), + response["meta"]["postTokenBalances"], + ) + ) + return TransactionScout( + timestamp, + signatures, + succeeded, + group_name, + accounts, + instructions, + messages, + pre_token_balances, + post_token_balances, + ) except Exception as exception: signature = "Unknown" - if response and ("transaction" in response) and ("signatures" in response["transaction"]) and len(response["transaction"]["signatures"]) > 0: + if ( + response + and ("transaction" in response) + and ("signatures" in response["transaction"]) + and len(response["transaction"]["signatures"]) > 0 + ): signature = ", ".join(response["transaction"]["signatures"]) - raise Exception(f"Exception fetching transaction '{signature}' - {traceback.format_exc()}", exception) + raise Exception( + f"Exception fetching transaction '{signature}' - {traceback.format_exc()}", + exception, + ) def __str__(self) -> str: - def format_tokens(account_token_values: typing.Sequence[OwnedInstrumentValue]) -> str: + def format_tokens( + account_token_values: typing.Sequence[OwnedInstrumentValue], + ) -> str: if len(account_token_values) == 0: return "None" return "\n ".join([f"{atv}" for atv in account_token_values]) - instruction_names = ", ".join([ins.instruction_type.name for ins in self.instructions]) + instruction_names = ", ".join( + [ins.instruction_type.name for ins in self.instructions] + ) signatures = "\n ".join(self.signatures) accounts = "\n ".join([f"{acc}" for acc in self.accounts]) messages = "\n ".join(self.messages) instructions = "\n ".join([f"{ins}" for ins in self.instructions]) - changes = OwnedInstrumentValue.changes(self.pre_token_balances, self.post_token_balances) + changes = OwnedInstrumentValue.changes( + self.pre_token_balances, self.post_token_balances + ) tokens_in = format_tokens(self.pre_token_balances) tokens_out = format_tokens(self.post_token_balances) token_changes = format_tokens(changes) @@ -220,9 +282,11 @@ def fetch_all_recent_transaction_signatures(context: Context) -> typing.Sequence before = None signature_results: typing.List[str] = [] while not all_fetched: - signatures = context.client.get_confirmed_signatures_for_address2(context.group_address, before=before) + signatures = context.client.get_confirmed_signatures_for_address2( + context.group_address, before=before + ) signature_results += signatures - if (len(signatures) == 0): + if len(signatures) == 0: all_fetched = True else: before = signature_results[-1] @@ -230,7 +294,11 @@ def fetch_all_recent_transaction_signatures(context: Context) -> typing.Sequence return signature_results -def mango_instruction_from_response(context: Context, all_accounts: typing.Sequence[PublicKey], instruction_data: typing.Dict[str, typing.Any]) -> typing.Optional["MangoInstruction"]: +def mango_instruction_from_response( + context: Context, + all_accounts: typing.Sequence[PublicKey], + instruction_data: typing.Dict[str, typing.Any], +) -> typing.Optional["MangoInstruction"]: program_account_index = instruction_data["programIdIndex"] if all_accounts[program_account_index] != context.mango_program_address: # It's an instruction, it's just not a Mango one. @@ -244,7 +312,8 @@ def mango_instruction_from_response(context: Context, all_accounts: typing.Seque parser = layouts.InstructionParsersByVariant[initial.variant] if parser is None: logging.warning( - f"Could not find instruction parser for variant {initial.variant} / {InstructionType(initial.variant)}.") + f"Could not find instruction parser for variant {initial.variant} / {InstructionType(initial.variant)}." + ) return None # A whole bunch of accounts are listed for a transaction. Some (or all) of them apply diff --git a/mango/valuation.py b/mango/valuation.py index ac2296f..b747702 100644 --- a/mango/valuation.py +++ b/mango/valuation.py @@ -30,14 +30,20 @@ from .token import Instrument, Token, SolToken class TokenValuation: - def __init__(self, raw_token_value: InstrumentValue, price_token_value: InstrumentValue, - value_token_value: InstrumentValue) -> None: + def __init__( + self, + raw_token_value: InstrumentValue, + price_token_value: InstrumentValue, + value_token_value: InstrumentValue, + ) -> None: self.raw: InstrumentValue = raw_token_value self.price: InstrumentValue = price_token_value self.value: InstrumentValue = value_token_value @staticmethod - def from_json_dict(context: Context, json: typing.Dict[str, typing.Any]) -> "TokenValuation": + def from_json_dict( + context: Context, json: typing.Dict[str, typing.Any] + ) -> "TokenValuation": symbol: str = json["symbol"] value_currency: str = json["valueCurrency"] balance: Decimal = Decimal(json["balance"]) @@ -48,36 +54,48 @@ class TokenValuation: raise Exception(f"Could not find token for symbol: {symbol}") currency_token = context.instrument_lookup.find_by_symbol(value_currency) if currency_token is None: - raise Exception(f"Could not find token for currency symbol: {value_currency}") + raise Exception( + f"Could not find token for currency symbol: {value_currency}" + ) raw_token_value: InstrumentValue = InstrumentValue(token, balance) price_token_value: InstrumentValue = InstrumentValue(currency_token, price) value_token_value: InstrumentValue = InstrumentValue(currency_token, value) return TokenValuation(raw_token_value, price_token_value, value_token_value) @staticmethod - def from_token_balance(context: Context, group: Group, cache: Cache, token_balance: InstrumentValue) -> "TokenValuation": + def from_token_balance( + context: Context, group: Group, cache: Cache, token_balance: InstrumentValue + ) -> "TokenValuation": token_to_lookup: Instrument = token_balance.token if token_balance.token == SolToken: token_to_lookup = context.instrument_lookup.find_by_symbol_or_raise("SOL") - cached_token_price: InstrumentValue = group.token_price_from_cache(cache, token_to_lookup) + cached_token_price: InstrumentValue = group.token_price_from_cache( + cache, token_to_lookup + ) balance_value: InstrumentValue = token_balance * cached_token_price return TokenValuation(token_balance, cached_token_price, balance_value) @staticmethod - def all_from_wallet(context: Context, group: Group, cache: Cache, address: PublicKey) -> typing.Sequence["TokenValuation"]: + def all_from_wallet( + context: Context, group: Group, cache: Cache, address: PublicKey + ) -> typing.Sequence["TokenValuation"]: balances: typing.List[InstrumentValue] = [] sol_balance = context.client.get_balance(address) balances += [InstrumentValue(SolToken, sol_balance)] for slot_token_bank in group.tokens: if isinstance(slot_token_bank.token, Token): - balance = InstrumentValue.fetch_total_value(context, address, slot_token_bank.token) + balance = InstrumentValue.fetch_total_value( + context, address, slot_token_bank.token + ) balances += [balance] wallet_tokens: typing.List[TokenValuation] = [] for balance in balances: if balance.value != 0: - wallet_tokens += [TokenValuation.from_token_balance(context, group, cache, balance)] + wallet_tokens += [ + TokenValuation.from_token_balance(context, group, cache, balance) + ] return wallet_tokens @@ -95,7 +113,9 @@ class TokenValuation: class AccountValuation: - def __init__(self, name: str, address: PublicKey, tokens: typing.Sequence[TokenValuation]) -> None: + def __init__( + self, name: str, address: PublicKey, tokens: typing.Sequence[TokenValuation] + ) -> None: self.name: str = name self.address: PublicKey = address self.tokens: typing.Sequence[TokenValuation] = tokens @@ -105,28 +125,45 @@ class AccountValuation: return sum((t.value for t in self.tokens[1:]), start=self.tokens[0].value) @staticmethod - def from_json_dict(context: Context, json: typing.Dict[str, typing.Any]) -> "AccountValuation": + def from_json_dict( + context: Context, json: typing.Dict[str, typing.Any] + ) -> "AccountValuation": name: str = json["name"] address: PublicKey = PublicKey(json["address"]) tokens: typing.List[TokenValuation] = [] for token_dict in json["tokens"]: - token_valuation: TokenValuation = TokenValuation.from_json_dict(context, token_dict) + token_valuation: TokenValuation = TokenValuation.from_json_dict( + context, token_dict + ) tokens += [token_valuation] return AccountValuation(name, address, tokens) @staticmethod - def from_account(context: Context, group: Group, account: Account, cache: Cache) -> "AccountValuation": - open_orders: typing.Dict[str, OpenOrders] = account.load_all_spot_open_orders(context) + def from_account( + context: Context, group: Group, account: Account, cache: Cache + ) -> "AccountValuation": + open_orders: typing.Dict[str, OpenOrders] = account.load_all_spot_open_orders( + context + ) token_values: typing.List[TokenValuation] = [] for asset in account.base_slots: - if (asset.net_value.value != 0) or ((asset.perp_account is not None) and not asset.perp_account.empty): - report: AccountInstrumentValues = AccountInstrumentValues.from_account_basket_base_token( - asset, open_orders, group) - asset_valuation = TokenValuation.from_token_balance(context, group, cache, report.net_value) + if (asset.net_value.value != 0) or ( + (asset.perp_account is not None) and not asset.perp_account.empty + ): + report: AccountInstrumentValues = ( + AccountInstrumentValues.from_account_basket_base_token( + asset, open_orders, group + ) + ) + asset_valuation = TokenValuation.from_token_balance( + context, group, cache, report.net_value + ) token_values += [asset_valuation] - quote_valuation = TokenValuation.from_token_balance(context, group, cache, account.shared_quote.net_value) + quote_valuation = TokenValuation.from_token_balance( + context, group, cache, account.shared_quote.net_value + ) token_values += [quote_valuation] return AccountValuation(account.info, account.address, token_values) @@ -138,12 +175,18 @@ class AccountValuation: "address": f"{self.address}", "value": f"{value.value:.8f}", "valueCurrency": value.token.symbol, - "tokens": list([tok.to_json_dict() for tok in self.tokens]) + "tokens": list([tok.to_json_dict() for tok in self.tokens]), } class Valuation: - def __init__(self, timestamp: datetime, address: PublicKey, wallet_tokens: typing.Sequence[TokenValuation], accounts: typing.Sequence[AccountValuation]): + def __init__( + self, + timestamp: datetime, + address: PublicKey, + wallet_tokens: typing.Sequence[TokenValuation], + accounts: typing.Sequence[AccountValuation], + ): self.timestamp: datetime = timestamp self.address: PublicKey = address self.wallet_tokens: typing.Sequence[TokenValuation] = wallet_tokens @@ -152,40 +195,52 @@ class Valuation: @property def value(self) -> InstrumentValue: wallet_tokens_value: InstrumentValue = sum( - (t.value for t in self.wallet_tokens[1:]), start=self.wallet_tokens[0].value) + (t.value for t in self.wallet_tokens[1:]), start=self.wallet_tokens[0].value + ) return sum((acc.value for acc in self.accounts), start=wallet_tokens_value) @staticmethod - def from_json_dict(context: Context, json: typing.Dict[str, typing.Any]) -> "Valuation": + def from_json_dict( + context: Context, json: typing.Dict[str, typing.Any] + ) -> "Valuation": timestamp: datetime = datetime.fromisoformat(json["timestamp"]) address: PublicKey = PublicKey(json["address"]) wallet_tokens: typing.List[TokenValuation] = [] for token_dict in json["wallet"]["tokens"]: - token_valuation: TokenValuation = TokenValuation.from_json_dict(context, token_dict) + token_valuation: TokenValuation = TokenValuation.from_json_dict( + context, token_dict + ) wallet_tokens += [token_valuation] accounts: typing.List[AccountValuation] = [] for account_dict in json["accounts"]: - account_valuation: AccountValuation = AccountValuation.from_json_dict(context, account_dict) + account_valuation: AccountValuation = AccountValuation.from_json_dict( + context, account_dict + ) accounts += [account_valuation] return Valuation(timestamp, address, wallet_tokens, accounts) @staticmethod - def from_wallet(context: Context, group: Group, cache: Cache, address: PublicKey) -> "Valuation": + def from_wallet( + context: Context, group: Group, cache: Cache, address: PublicKey + ) -> "Valuation": spl_tokens = TokenValuation.all_from_wallet(context, group, cache, address) mango_accounts = Account.load_all_for_owner(context, address, group) account_valuations = [] for account in mango_accounts: - account_valuations += [AccountValuation.from_account(context, group, account, cache)] + account_valuations += [ + AccountValuation.from_account(context, group, account, cache) + ] return Valuation(datetime.now(), address, spl_tokens, account_valuations) def to_json_dict(self) -> typing.Dict[str, typing.Any]: value: InstrumentValue = self.value wallet_value: InstrumentValue = sum( - (t.value for t in self.wallet_tokens[1:]), start=self.wallet_tokens[0].value) + (t.value for t in self.wallet_tokens[1:]), start=self.wallet_tokens[0].value + ) return { "timestamp": self.timestamp.isoformat(), "address": f"{self.address}", @@ -194,23 +249,30 @@ class Valuation: "wallet": { "value": f"{wallet_value.value:.8f}", "valueCurrency": wallet_value.token.symbol, - "tokens": list([tok.to_json_dict() for tok in self.wallet_tokens]) + "tokens": list([tok.to_json_dict() for tok in self.wallet_tokens]), }, - "accounts": list([acc.to_json_dict() for acc in self.accounts]) + "accounts": list([acc.to_json_dict() for acc in self.accounts]), } def __str__(self) -> str: address: str = f"{self.address}:" wallet_total: InstrumentValue = sum( - (t.value for t in self.wallet_tokens[1:]), start=self.wallet_tokens[0].value) + (t.value for t in self.wallet_tokens[1:]), start=self.wallet_tokens[0].value + ) accounts: typing.List[str] = [] for account in self.accounts: - account_tokens: str = "\n ".join([f"{item}" for item in account.tokens]) - accounts += [f"""Account '{account.name}' (total: {account.value}): - {account_tokens}"""] + account_tokens: str = "\n ".join( + [f"{item}" for item in account.tokens] + ) + accounts += [ + f"""Account '{account.name}' (total: {account.value}): + {account_tokens}""" + ] accounts_tokens: str = "\n ".join(accounts) - wallet_tokens: str = "\n ".join([f"{item}" for item in self.wallet_tokens]) + wallet_tokens: str = "\n ".join( + [f"{item}" for item in self.wallet_tokens] + ) return f"""ยซ Valuation of {address:<47} {self.value} Wallet Tokens (total: {wallet_total}): {wallet_tokens} diff --git a/mango/wallet.py b/mango/wallet.py index 209b525..cdddbf0 100644 --- a/mango/wallet.py +++ b/mango/wallet.py @@ -95,8 +95,12 @@ class Wallet: # @staticmethod def add_command_line_parameters(parser: argparse.ArgumentParser) -> None: - parser.add_argument("--id-file", type=str, default=_DEFAULT_WALLET_FILENAME, - help="file containing the JSON-formatted wallet private key") + parser.add_argument( + "--id-file", + type=str, + default=_DEFAULT_WALLET_FILENAME, + help="file containing the JSON-formatted wallet private 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 @@ -105,10 +109,14 @@ class Wallet: # It then uses those parameters to create a properly-configured `Wallet` object. # @staticmethod - def from_command_line_parameters(args: argparse.Namespace) -> typing.Optional["Wallet"]: + def from_command_line_parameters( + args: argparse.Namespace, + ) -> typing.Optional["Wallet"]: # We always have an args.id_file (because we specify a default) so check for the environment # variable and give it priority. - environment_secret_key = os.environ.get("KEYPAIR") or os.environ.get("SECRET_KEY") + environment_secret_key = os.environ.get("KEYPAIR") or os.environ.get( + "SECRET_KEY" + ) if environment_secret_key is not None: secret_key_bytes = json.loads(environment_secret_key) if len(secret_key_bytes) >= 32: diff --git a/mango/walletbalancer.py b/mango/walletbalancer.py index 8285a43..497a590 100644 --- a/mango/walletbalancer.py +++ b/mango/walletbalancer.py @@ -73,8 +73,12 @@ class TargetBalance(metaclass=abc.ABCMeta): self.symbol = symbol @abc.abstractmethod - def resolve(self, instrument: Instrument, current_price: Decimal, total_value: Decimal) -> InstrumentValue: - raise NotImplementedError("TargetBalance.resolve() is not implemented on the base type.") + def resolve( + self, instrument: Instrument, current_price: Decimal, total_value: Decimal + ) -> InstrumentValue: + raise NotImplementedError( + "TargetBalance.resolve() is not implemented on the base type." + ) def __repr__(self) -> str: return f"{self}" @@ -89,7 +93,9 @@ class FixedTargetBalance(TargetBalance): super().__init__(symbol) self.value = value - def resolve(self, instrument: Instrument, current_price: Decimal, total_value: Decimal) -> InstrumentValue: + def resolve( + self, instrument: Instrument, current_price: Decimal, total_value: Decimal + ) -> InstrumentValue: return InstrumentValue(instrument, self.value) def __str__(self) -> str: @@ -113,7 +119,9 @@ class PercentageTargetBalance(TargetBalance): super().__init__(symbol) self.target_fraction = target_percentage / 100 - def resolve(self, instrument: Instrument, current_price: Decimal, total_value: Decimal) -> InstrumentValue: + def resolve( + self, instrument: Instrument, current_price: Decimal, total_value: Decimal + ) -> InstrumentValue: target_value = total_value * self.target_fraction target_size = target_value / current_price return InstrumentValue(instrument, target_size) @@ -143,11 +151,13 @@ def parse_target_balance(to_parse: str) -> TargetBalance: numeric_value = Decimal(numeric_value_string) except Exception as exception: raise Exception( - f"Could not parse '{numeric_value_string}' as a decimal number. It should be formatted as a decimal number, e.g. '2.345', with no surrounding spaces.") from exception + f"Could not parse '{numeric_value_string}' as a decimal number. It should be formatted as a decimal number, e.g. '2.345', with no surrounding spaces." + ) from exception if len(values) > 2: raise Exception( - f"Could not parse '{value}' as a decimal percentage. It should be formatted as a decimal number followed by a percentage sign, e.g. '30%', with no surrounding spaces.") + f"Could not parse '{value}' as a decimal percentage. It should be formatted as a decimal number followed by a percentage sign, e.g. '30%', with no surrounding spaces." + ) if len(values) == 1: return FixedTargetBalance(symbol, numeric_value) @@ -176,14 +186,16 @@ def parse_fixed_target_balance(to_parse: str) -> TargetBalance: values = value.split("%") if len(values) > 1: raise Exception( - f"Could not parse '{value}' as a decimal target. (Percentage targets are not allowed in this context.)") + f"Could not parse '{value}' as a decimal target. (Percentage targets are not allowed in this context.)" + ) numeric_value_string = values[0] try: numeric_value = Decimal(numeric_value_string) except Exception as exception: raise Exception( - f"Could not parse '{numeric_value_string}' as a decimal number. It should be formatted as a decimal number, e.g. '2.345', with no surrounding spaces.") from exception + f"Could not parse '{numeric_value_string}' as a decimal number. It should be formatted as a decimal number, e.g. '2.345', with no surrounding spaces." + ) from exception return FixedTargetBalance(symbol, numeric_value) @@ -198,7 +210,9 @@ def parse_fixed_target_balance(to_parse: str) -> TargetBalance: # really care that much as long as we have SELLs before BUYs. (We could, later, take price # into account for this sorting but we don't need to now so we don't.) # -def sort_changes_for_trades(changes: typing.Sequence[InstrumentValue]) -> typing.Sequence[InstrumentValue]: +def sort_changes_for_trades( + changes: typing.Sequence[InstrumentValue], +) -> typing.Sequence[InstrumentValue]: return sorted(changes, key=lambda change: change.value) @@ -206,7 +220,10 @@ def sort_changes_for_trades(changes: typing.Sequence[InstrumentValue]) -> typing # # Takes a list of current balances, and a list of desired balances, and returns the list of changes required to get us to the desired balances. # -def calculate_required_balance_changes(current_balances: typing.Sequence[InstrumentValue], desired_balances: typing.Sequence[InstrumentValue]) -> typing.Sequence[InstrumentValue]: +def calculate_required_balance_changes( + current_balances: typing.Sequence[InstrumentValue], + desired_balances: typing.Sequence[InstrumentValue], +) -> typing.Sequence[InstrumentValue]: changes: typing.List[InstrumentValue] = [] for desired in desired_balances: current = InstrumentValue.find_by_token(current_balances, desired.token) @@ -229,8 +246,12 @@ def calculate_required_balance_changes(current_balances: typing.Sequence[Instrum # easier to reason about. # class FilterSmallChanges: - def __init__(self, action_threshold: Decimal, balances: typing.Sequence[InstrumentValue], - prices: typing.Sequence[InstrumentValue]) -> None: + def __init__( + self, + action_threshold: Decimal, + balances: typing.Sequence[InstrumentValue], + prices: typing.Sequence[InstrumentValue], + ) -> None: self._logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.prices: typing.Dict[str, InstrumentValue] = {} total = Decimal(0) @@ -241,7 +262,8 @@ class FilterSmallChanges: self.total_balance = total self.action_threshold_value = total * action_threshold self._logger.info( - f"Wallet total balance of {total:,.8f} gives action threshold: {self.action_threshold_value:,.8f}") + f"Wallet total balance of {total:,.8f} gives action threshold: {self.action_threshold_value:,.8f}" + ) def allow(self, token_value: InstrumentValue) -> bool: price = self.prices[token_value.token.symbol] @@ -250,7 +272,8 @@ class FilterSmallChanges: result = absolute_value > self.action_threshold_value self._logger.info( - f"Worth doing? {result}. {token_value.token.name} trade is worth: {absolute_value:,.8f}, threshold is: {self.action_threshold_value:,.8f}.") + f"Worth doing? {result}. {token_value.token.name} trade is worth: {absolute_value:,.8f}, threshold is: {self.action_threshold_value:,.8f}." + ) return result @@ -279,8 +302,12 @@ class WalletBalancer(metaclass=abc.ABCMeta): self._logger: logging.Logger = logging.getLogger(self.__class__.__name__) @abc.abstractmethod - def balance(self, context: Context, prices: typing.Sequence[InstrumentValue]) -> None: - raise NotImplementedError("WalletBalancer.balance() is not implemented on the base type.") + def balance( + self, context: Context, prices: typing.Sequence[InstrumentValue] + ) -> None: + raise NotImplementedError( + "WalletBalancer.balance() is not implemented on the base type." + ) # # ๐Ÿฅญ NullWalletBalancer class @@ -292,7 +319,9 @@ class NullWalletBalancer(WalletBalancer): def __init__(self) -> None: super().__init__() - def balance(self, context: Context, prices: typing.Sequence[InstrumentValue]) -> None: + def balance( + self, context: Context, prices: typing.Sequence[InstrumentValue] + ) -> None: pass @@ -301,8 +330,14 @@ class NullWalletBalancer(WalletBalancer): # This is the high-level class that does much of the work. # class LiveWalletBalancer(WalletBalancer): - def __init__(self, wallet: Wallet, quote_token: Token, trade_executor: TradeExecutor, - targets: typing.Sequence[TargetBalance], action_threshold: Decimal) -> None: + def __init__( + self, + wallet: Wallet, + quote_token: Token, + trade_executor: TradeExecutor, + targets: typing.Sequence[TargetBalance], + action_threshold: Decimal, + ) -> None: super().__init__() self.wallet: Wallet = wallet self.quote_token: Token = quote_token @@ -310,7 +345,9 @@ class LiveWalletBalancer(WalletBalancer): self.targets: typing.Sequence[TargetBalance] = targets self.action_threshold: Decimal = action_threshold - def balance(self, context: Context, prices: typing.Sequence[InstrumentValue]) -> None: + def balance( + self, context: Context, prices: typing.Sequence[InstrumentValue] + ) -> None: padding = "\n " def balances_report(balances: typing.Sequence[InstrumentValue]) -> str: @@ -320,7 +357,9 @@ class LiveWalletBalancer(WalletBalancer): for target_balance in self.targets: token = context.instrument_lookup.find_by_symbol(target_balance.symbol) if token is None: - raise Exception(f"Could not find details of token {target_balance.symbol}.") + raise Exception( + f"Could not find details of token {target_balance.symbol}." + ) tokens += [Token.ensure(token)] tokens += [self.quote_token] @@ -331,7 +370,9 @@ class LiveWalletBalancer(WalletBalancer): value = bal.value * price.value total_value += value self._logger.info(f"Starting balances: {padding}{balances_report(balances)}") - total_token_value: InstrumentValue = InstrumentValue(self.quote_token, total_value) + total_token_value: InstrumentValue = InstrumentValue( + self.quote_token, total_value + ) self._logger.info(f"Total: {total_token_value}") resolved_targets: typing.List[InstrumentValue] = [] @@ -340,11 +381,15 @@ class LiveWalletBalancer(WalletBalancer): resolved_targets += [target.resolve(price.token, price.value, total_value)] balance_changes = calculate_required_balance_changes(balances, resolved_targets) - self._logger.info(f"Desired balance changes: {padding}{balances_report(balance_changes)}") + self._logger.info( + f"Desired balance changes: {padding}{balances_report(balance_changes)}" + ) dont_bother = FilterSmallChanges(self.action_threshold, balances, prices) filtered_changes = list(filter(dont_bother.allow, balance_changes)) - self._logger.info(f"Filtered balance changes: {padding}{balances_report(filtered_changes)}") + self._logger.info( + f"Filtered balance changes: {padding}{balances_report(filtered_changes)}" + ) if len(filtered_changes) == 0: self._logger.info("No balance changes to make.") return @@ -352,7 +397,9 @@ class LiveWalletBalancer(WalletBalancer): sorted_changes = sort_changes_for_trades(filtered_changes) self._make_changes(sorted_changes) updated_balances = self._fetch_balances(context, tokens) - self._logger.info(f"Finishing balances: {padding}{balances_report(updated_balances)}") + self._logger.info( + f"Finishing balances: {padding}{balances_report(updated_balances)}" + ) def _make_changes(self, balance_changes: typing.Sequence[InstrumentValue]) -> None: quote = self.quote_token.symbol @@ -363,10 +410,14 @@ class LiveWalletBalancer(WalletBalancer): else: self.trade_executor.buy(market_symbol, change.value.copy_abs()) - def _fetch_balances(self, context: Context, tokens: typing.Sequence[Token]) -> typing.Sequence[InstrumentValue]: + def _fetch_balances( + self, context: Context, tokens: typing.Sequence[Token] + ) -> typing.Sequence[InstrumentValue]: balances: typing.List[InstrumentValue] = [] for token in tokens: - balance = InstrumentValue.fetch_total_value(context, self.wallet.address, token) + balance = InstrumentValue.fetch_total_value( + context, self.wallet.address, token + ) balances += [balance] return balances @@ -377,8 +428,14 @@ class LiveWalletBalancer(WalletBalancer): # This is the high-level class that does much of the work. # class LiveAccountBalancer(WalletBalancer): - def __init__(self, account: Account, group: Group, trade_executor: TradeExecutor, - targets: typing.Sequence[TargetBalance], action_threshold: Decimal) -> None: + def __init__( + self, + account: Account, + group: Group, + trade_executor: TradeExecutor, + targets: typing.Sequence[TargetBalance], + action_threshold: Decimal, + ) -> None: super().__init__() self.account: Account = account self.group: Group = group @@ -386,7 +443,9 @@ class LiveAccountBalancer(WalletBalancer): self.targets: typing.Sequence[TargetBalance] = targets self.action_threshold: Decimal = action_threshold - def balance(self, context: Context, prices: typing.Sequence[InstrumentValue]) -> None: + def balance( + self, context: Context, prices: typing.Sequence[InstrumentValue] + ) -> None: padding = "\n " def balances_report(balances: typing.Sequence[InstrumentValue]) -> str: @@ -408,11 +467,15 @@ class LiveAccountBalancer(WalletBalancer): resolved_targets += [target.resolve(price.token, price.value, total_value)] balance_changes = calculate_required_balance_changes(balances, resolved_targets) - self._logger.info(f"Desired balance changes: {padding}{balances_report(balance_changes)}") + self._logger.info( + f"Desired balance changes: {padding}{balances_report(balance_changes)}" + ) dont_bother = FilterSmallChanges(self.action_threshold, balances, prices) filtered_changes = list(filter(dont_bother.allow, balance_changes)) - self._logger.info(f"Worthwhile balance changes: {padding}{balances_report(filtered_changes)}") + self._logger.info( + f"Worthwhile balance changes: {padding}{balances_report(filtered_changes)}" + ) if len(filtered_changes) == 0: self._logger.info("No balance changes to make.") return @@ -420,9 +483,15 @@ class LiveAccountBalancer(WalletBalancer): sorted_changes = sort_changes_for_trades(filtered_changes) self._make_changes(sorted_changes) - updated_account: Account = Account.load(context, self.account.address, self.group) - updated_balances = [basket_token.net_value for basket_token in updated_account.base_slots] - self._logger.info(f"Finishing balances: {padding}{balances_report(updated_balances)}") + updated_account: Account = Account.load( + context, self.account.address, self.group + ) + updated_balances = [ + basket_token.net_value for basket_token in updated_account.base_slots + ] + self._logger.info( + f"Finishing balances: {padding}{balances_report(updated_balances)}" + ) def _make_changes(self, balance_changes: typing.Sequence[InstrumentValue]) -> None: quote = self.account.shared_quote_token.symbol diff --git a/mango/watchers.py b/mango/watchers.py index d090746..d98a2d6 100644 --- a/mango/watchers.py +++ b/mango/watchers.py @@ -50,12 +50,26 @@ from .tokenaccount import TokenAccount from .token import Instrument, Token from .wallet import Wallet from .watcher import Watcher, LamdaUpdateWatcher -from .websocketsubscription import WebSocketAccountSubscription, WebSocketSubscription, WebSocketSubscriptionManager +from .websocketsubscription import ( + WebSocketAccountSubscription, + WebSocketSubscription, + WebSocketSubscriptionManager, +) -def build_group_watcher(context: Context, manager: WebSocketSubscriptionManager, health_check: HealthCheck, group: Group) -> Watcher[Group]: +def build_group_watcher( + context: Context, + manager: WebSocketSubscriptionManager, + health_check: HealthCheck, + group: Group, +) -> Watcher[Group]: group_subscription = WebSocketAccountSubscription[Group]( - context, group.address, lambda account_info: Group.parse(account_info, group.name, context.instrument_lookup, context.market_lookup)) + context, + group.address, + lambda account_info: Group.parse( + account_info, group.name, context.instrument_lookup, context.market_lookup + ), + ) manager.add(group_subscription) latest_group_observer = LatestItemObserverSubscriber[Group](group) group_subscription.publisher.subscribe(latest_group_observer) @@ -63,9 +77,21 @@ def build_group_watcher(context: Context, manager: WebSocketSubscriptionManager, return latest_group_observer -def build_account_watcher(context: Context, manager: WebSocketSubscriptionManager, health_check: HealthCheck, account: Account, group_observer: Watcher[Group], cache_observer: Watcher[Cache]) -> typing.Tuple[WebSocketSubscription[Account], Watcher[Account]]: +def build_account_watcher( + context: Context, + manager: WebSocketSubscriptionManager, + health_check: HealthCheck, + account: Account, + group_observer: Watcher[Group], + cache_observer: Watcher[Cache], +) -> typing.Tuple[WebSocketSubscription[Account], Watcher[Account]]: account_subscription = WebSocketAccountSubscription[Account]( - context, account.address, lambda account_info: Account.parse(account_info, group_observer.latest, cache_observer.latest)) + context, + account.address, + lambda account_info: Account.parse( + account_info, group_observer.latest, cache_observer.latest + ), + ) manager.add(account_subscription) latest_account_observer = LatestItemObserverSubscriber[Account](account) account_subscription.publisher.subscribe(latest_account_observer) @@ -73,9 +99,16 @@ def build_account_watcher(context: Context, manager: WebSocketSubscriptionManage return account_subscription, latest_account_observer -def build_cache_watcher(context: Context, manager: WebSocketSubscriptionManager, health_check: HealthCheck, cache: Cache, group: Group) -> Watcher[Cache]: +def build_cache_watcher( + context: Context, + manager: WebSocketSubscriptionManager, + health_check: HealthCheck, + cache: Cache, + group: Group, +) -> Watcher[Cache]: cache_subscription = WebSocketAccountSubscription[Cache]( - context, group.cache, lambda account_info: Cache.parse(account_info)) + context, group.cache, lambda account_info: Cache.parse(account_info) + ) manager.add(cache_subscription) latest_cache_observer = LatestItemObserverSubscriber[Cache](cache) cache_subscription.publisher.subscribe(latest_cache_observer) @@ -83,80 +116,161 @@ def build_cache_watcher(context: Context, manager: WebSocketSubscriptionManager, return latest_cache_observer -def build_spot_open_orders_watcher(context: Context, manager: WebSocketSubscriptionManager, health_check: HealthCheck, wallet: Wallet, account: Account, group: Group, spot_market: SpotMarket) -> Watcher[OpenOrders]: +def build_spot_open_orders_watcher( + context: Context, + manager: WebSocketSubscriptionManager, + health_check: HealthCheck, + wallet: Wallet, + account: Account, + group: Group, + spot_market: SpotMarket, +) -> Watcher[OpenOrders]: market_index = group.slot_by_spot_market_address(spot_market.address).index open_orders_address = account.spot_open_orders_by_index[market_index] if open_orders_address is None: - spot_market_instruction_builder: SpotMarketInstructionBuilder = SpotMarketInstructionBuilder.load( - context, wallet, spot_market, spot_market.group, account) + spot_market_instruction_builder: SpotMarketInstructionBuilder = ( + SpotMarketInstructionBuilder.load( + context, wallet, spot_market, spot_market.group, account + ) + ) market_operations: SpotMarketOperations = SpotMarketOperations( - context, wallet, account, spot_market_instruction_builder) + context, wallet, account, spot_market_instruction_builder + ) open_orders_address = market_operations.create_openorders() - logging.info(f"Created {spot_market.symbol} OpenOrders at: {open_orders_address}") + logging.info( + f"Created {spot_market.symbol} OpenOrders at: {open_orders_address}" + ) spot_open_orders_subscription = WebSocketAccountSubscription[OpenOrders]( - context, open_orders_address, lambda account_info: OpenOrders.parse(account_info, spot_market.base.decimals, spot_market.quote.decimals)) + context, + open_orders_address, + lambda account_info: OpenOrders.parse( + account_info, spot_market.base.decimals, spot_market.quote.decimals + ), + ) manager.add(spot_open_orders_subscription) initial_spot_open_orders = OpenOrders.load( - context, open_orders_address, spot_market.base.decimals, spot_market.quote.decimals) + context, + open_orders_address, + spot_market.base.decimals, + spot_market.quote.decimals, + ) latest_open_orders_observer = LatestItemObserverSubscriber[OpenOrders]( - initial_spot_open_orders) + initial_spot_open_orders + ) spot_open_orders_subscription.publisher.subscribe(latest_open_orders_observer) - health_check.add("open_orders_subscription", spot_open_orders_subscription.publisher) + health_check.add( + "open_orders_subscription", spot_open_orders_subscription.publisher + ) return latest_open_orders_observer -def build_serum_open_orders_watcher(context: Context, manager: WebSocketSubscriptionManager, health_check: HealthCheck, serum_market: SerumMarket, wallet: Wallet) -> Watcher[PlacedOrdersContainer]: +def build_serum_open_orders_watcher( + context: Context, + manager: WebSocketSubscriptionManager, + health_check: HealthCheck, + serum_market: SerumMarket, + wallet: Wallet, +) -> Watcher[PlacedOrdersContainer]: all_open_orders = OpenOrders.load_for_market_and_owner( - context, serum_market.address, wallet.address, context.serum_program_address, serum_market.base.decimals, serum_market.quote.decimals) + context, + serum_market.address, + wallet.address, + context.serum_program_address, + serum_market.base.decimals, + serum_market.quote.decimals, + ) if len(all_open_orders) > 0: initial_serum_open_orders: OpenOrders = all_open_orders[0] open_orders_address = initial_serum_open_orders.address else: - raw_market = PySerumMarket.load(context.client.compatible_client, serum_market.address) + raw_market = PySerumMarket.load( + context.client.compatible_client, serum_market.address + ) create_open_orders = build_create_serum_open_orders_instructions( - context, wallet, raw_market) + context, wallet, raw_market + ) open_orders_address = create_open_orders.signers[0].public_key - logging.info(f"Creating OpenOrders account for market {serum_market.symbol} at {open_orders_address}.") + logging.info( + f"Creating OpenOrders account for market {serum_market.symbol} at {open_orders_address}." + ) signers: CombinableInstructions = CombinableInstructions.from_wallet(wallet) transaction_ids = (signers + create_open_orders).execute(context) context.client.wait_for_confirmation(transaction_ids) initial_serum_open_orders = OpenOrders.load( - context, open_orders_address, serum_market.base.decimals, serum_market.quote.decimals) + context, + open_orders_address, + serum_market.base.decimals, + serum_market.quote.decimals, + ) serum_open_orders_subscription = WebSocketAccountSubscription[OpenOrders]( - context, open_orders_address, lambda account_info: OpenOrders.parse(account_info, serum_market.base.decimals, serum_market.quote.decimals)) + context, + open_orders_address, + lambda account_info: OpenOrders.parse( + account_info, serum_market.base.decimals, serum_market.quote.decimals + ), + ) manager.add(serum_open_orders_subscription) - latest_serum_open_orders_observer = LatestItemObserverSubscriber[PlacedOrdersContainer]( - initial_serum_open_orders) - serum_open_orders_subscription.publisher.subscribe(latest_serum_open_orders_observer) - health_check.add("open_orders_subscription", serum_open_orders_subscription.publisher) + latest_serum_open_orders_observer = LatestItemObserverSubscriber[ + PlacedOrdersContainer + ](initial_serum_open_orders) + serum_open_orders_subscription.publisher.subscribe( + latest_serum_open_orders_observer + ) + health_check.add( + "open_orders_subscription", serum_open_orders_subscription.publisher + ) return latest_serum_open_orders_observer -def build_perp_open_orders_watcher(context: Context, manager: WebSocketSubscriptionManager, health_check: HealthCheck, perp_market: PerpMarket, account: Account, group: Group, account_subscription: WebSocketSubscription[Account]) -> Watcher[PlacedOrdersContainer]: +def build_perp_open_orders_watcher( + context: Context, + manager: WebSocketSubscriptionManager, + health_check: HealthCheck, + perp_market: PerpMarket, + account: Account, + group: Group, + account_subscription: WebSocketSubscription[Account], +) -> Watcher[PlacedOrdersContainer]: slot: GroupSlot = group.slot_by_perp_market_address(perp_market.address) index: int = slot.index initial_perp_account = account.perp_accounts_by_index[slot.index] if initial_perp_account is None: - raise Exception(f"Could not find perp account at index {slot.index} of account {account.address}.") + raise Exception( + f"Could not find perp account at index {slot.index} of account {account.address}." + ) initial_open_orders = initial_perp_account.open_orders - latest_open_orders_observer = LatestItemObserverSubscriber[PlacedOrdersContainer](initial_open_orders) + latest_open_orders_observer = LatestItemObserverSubscriber[PlacedOrdersContainer]( + initial_open_orders + ) account_subscription.publisher.subscribe( - on_next=lambda updated_account: latest_open_orders_observer.on_next(updated_account.perp_accounts_by_index[index].open_orders)) # type: ignore[call-arg] + on_next=lambda updated_account: latest_open_orders_observer.on_next( + updated_account.perp_accounts_by_index[index].open_orders + ) + ) # type: ignore[call-arg] health_check.add("open_orders_subscription", account_subscription.publisher) return latest_open_orders_observer -def build_price_watcher(context: Context, manager: WebSocketSubscriptionManager, health_check: HealthCheck, disposer: DisposePropagator, provider_name: str, market: Market) -> LatestItemObserverSubscriber[Price]: +def build_price_watcher( + context: Context, + manager: WebSocketSubscriptionManager, + health_check: HealthCheck, + disposer: DisposePropagator, + provider_name: str, + market: Market, +) -> LatestItemObserverSubscriber[Price]: oracle_provider: OracleProvider = create_oracle_provider(context, provider_name) 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 {provider_name}.") + raise Exception( + f"Could not find oracle for market {market.symbol} from provider {provider_name}." + ) initial_price = oracle.fetch_price(context) price_feed = oracle.to_streaming_observable(context) @@ -167,27 +281,55 @@ def build_price_watcher(context: Context, manager: WebSocketSubscriptionManager, return latest_price_observer -def build_serum_inventory_watcher(context: Context, manager: WebSocketSubscriptionManager, health_check: HealthCheck, disposer: DisposePropagator, wallet: Wallet, market: SerumMarket, price_watcher: Watcher[Price]) -> Watcher[Inventory]: +def build_serum_inventory_watcher( + context: Context, + manager: WebSocketSubscriptionManager, + health_check: HealthCheck, + disposer: DisposePropagator, + wallet: Wallet, + market: SerumMarket, + price_watcher: Watcher[Price], +) -> Watcher[Inventory]: base_account = TokenAccount.fetch_largest_for_owner_and_token( - context, wallet.address, market.base) + context, wallet.address, market.base + ) if base_account is None: - raise Exception(f"Could not find token account owned by {wallet.address} for base token {market.base}.") + raise Exception( + f"Could not find token account owned by {wallet.address} for base token {market.base}." + ) base_token_subscription = WebSocketAccountSubscription[TokenAccount]( - context, base_account.address, lambda account_info: TokenAccount.parse(account_info, market.base)) + context, + base_account.address, + lambda account_info: TokenAccount.parse(account_info, market.base), + ) manager.add(base_token_subscription) - latest_base_token_account_observer = LatestItemObserverSubscriber[TokenAccount](base_account) - base_subscription_disposable = base_token_subscription.publisher.subscribe(latest_base_token_account_observer) + latest_base_token_account_observer = LatestItemObserverSubscriber[TokenAccount]( + base_account + ) + base_subscription_disposable = base_token_subscription.publisher.subscribe( + latest_base_token_account_observer + ) disposer.add_disposable(base_subscription_disposable) quote_account = TokenAccount.fetch_largest_for_owner_and_token( - context, wallet.address, market.quote) + context, wallet.address, market.quote + ) if quote_account is None: - raise Exception(f"Could not find token account owned by {wallet.address} for quote token {market.quote}.") + raise Exception( + f"Could not find token account owned by {wallet.address} for quote token {market.quote}." + ) quote_token_subscription = WebSocketAccountSubscription[TokenAccount]( - context, quote_account.address, lambda account_info: TokenAccount.parse(account_info, market.quote)) + context, + quote_account.address, + lambda account_info: TokenAccount.parse(account_info, market.quote), + ) manager.add(quote_token_subscription) - latest_quote_token_account_observer = LatestItemObserverSubscriber[TokenAccount](quote_account) - quote_subscription_disposable = quote_token_subscription.publisher.subscribe(latest_quote_token_account_observer) + latest_quote_token_account_observer = LatestItemObserverSubscriber[TokenAccount]( + quote_account + ) + quote_subscription_disposable = quote_token_subscription.publisher.subscribe( + latest_quote_token_account_observer + ) disposer.add_disposable(quote_subscription_disposable) # Serum markets don't accrue MNGO liquidity incentives @@ -197,30 +339,50 @@ def build_serum_inventory_watcher(context: Context, manager: WebSocketSubscripti mngo_accrued: InstrumentValue = InstrumentValue(Token.ensure(mngo), Decimal(0)) def serum_inventory_accessor() -> Inventory: - available: Decimal = (latest_base_token_account_observer.latest.value.value * price_watcher.latest.mid_price) + \ - latest_quote_token_account_observer.latest.value.value + available: Decimal = ( + latest_base_token_account_observer.latest.value.value + * price_watcher.latest.mid_price + ) + latest_quote_token_account_observer.latest.value.value available_collateral: InstrumentValue = InstrumentValue( - latest_quote_token_account_observer.latest.value.token, available) - return Inventory(InventorySource.SPL_TOKENS, mngo_accrued, - available_collateral, - latest_base_token_account_observer.latest.value, - latest_quote_token_account_observer.latest.value) + latest_quote_token_account_observer.latest.value.token, available + ) + return Inventory( + InventorySource.SPL_TOKENS, + mngo_accrued, + available_collateral, + latest_base_token_account_observer.latest.value, + latest_quote_token_account_observer.latest.value, + ) return LamdaUpdateWatcher(serum_inventory_accessor) -def build_orderbook_watcher(context: Context, manager: WebSocketSubscriptionManager, health_check: HealthCheck, market: LoadedMarket) -> Watcher[OrderBook]: +def build_orderbook_watcher( + context: Context, + manager: WebSocketSubscriptionManager, + health_check: HealthCheck, + market: LoadedMarket, +) -> Watcher[OrderBook]: orderbook_addresses: typing.List[PublicKey] = [ market.bids_address, - market.asks_address + market.asks_address, ] orderbook_infos = AccountInfo.load_multiple(context, orderbook_addresses) - if len(orderbook_infos) != 2 or orderbook_infos[0] is None or orderbook_infos[1] is None: - raise Exception(f"Could not find {market.symbol} order book at addresses {orderbook_addresses}.") + if ( + len(orderbook_infos) != 2 + or orderbook_infos[0] is None + or orderbook_infos[1] is None + ): + raise Exception( + f"Could not find {market.symbol} order book at addresses {orderbook_addresses}." + ) - initial_orderbook: OrderBook = market.parse_account_infos_to_orderbook(orderbook_infos[0], orderbook_infos[1]) + initial_orderbook: OrderBook = market.parse_account_infos_to_orderbook( + orderbook_infos[0], orderbook_infos[1] + ) updatable_orderbook: OrderBook = market.parse_account_infos_to_orderbook( - orderbook_infos[0], orderbook_infos[1]) + orderbook_infos[0], orderbook_infos[1] + ) def _update_bids(account_info: AccountInfo) -> OrderBook: new_bids = market.parse_account_info_to_orders(account_info) @@ -231,9 +393,14 @@ def build_orderbook_watcher(context: Context, manager: WebSocketSubscriptionMana new_asks = market.parse_account_info_to_orders(account_info) updatable_orderbook.asks = new_asks return updatable_orderbook - bids_subscription = WebSocketAccountSubscription[OrderBook](context, orderbook_addresses[0], _update_bids) + + bids_subscription = WebSocketAccountSubscription[OrderBook]( + context, orderbook_addresses[0], _update_bids + ) manager.add(bids_subscription) - asks_subscription = WebSocketAccountSubscription[OrderBook](context, orderbook_addresses[1], _update_asks) + asks_subscription = WebSocketAccountSubscription[OrderBook]( + context, orderbook_addresses[1], _update_asks + ) manager.add(asks_subscription) orderbook_observer = LatestItemObserverSubscriber[OrderBook](initial_orderbook) @@ -245,10 +412,20 @@ def build_orderbook_watcher(context: Context, manager: WebSocketSubscriptionMana return orderbook_observer -def build_serum_event_queue_watcher(context: Context, manager: WebSocketSubscriptionManager, health_check: HealthCheck, serum_market: SerumMarket) -> Watcher[EventQueue]: - initial: EventQueue = SerumEventQueue.load(context, serum_market.event_queue_address) +def build_serum_event_queue_watcher( + context: Context, + manager: WebSocketSubscriptionManager, + health_check: HealthCheck, + serum_market: SerumMarket, +) -> Watcher[EventQueue]: + initial: EventQueue = SerumEventQueue.load( + context, serum_market.event_queue_address + ) subscription = WebSocketAccountSubscription[EventQueue]( - context, serum_market.event_queue_address, lambda account_info: SerumEventQueue.parse(account_info)) + context, + serum_market.event_queue_address, + lambda account_info: SerumEventQueue.parse(account_info), + ) manager.add(subscription) latest_observer = LatestItemObserverSubscriber[EventQueue](initial) subscription.publisher.subscribe(latest_observer) @@ -256,10 +433,18 @@ def build_serum_event_queue_watcher(context: Context, manager: WebSocketSubscrip return latest_observer -def build_spot_event_queue_watcher(context: Context, manager: WebSocketSubscriptionManager, health_check: HealthCheck, spot_market: SpotMarket) -> Watcher[EventQueue]: +def build_spot_event_queue_watcher( + context: Context, + manager: WebSocketSubscriptionManager, + health_check: HealthCheck, + spot_market: SpotMarket, +) -> Watcher[EventQueue]: initial: EventQueue = SerumEventQueue.load(context, spot_market.event_queue_address) subscription = WebSocketAccountSubscription[EventQueue]( - context, spot_market.event_queue_address, lambda account_info: SerumEventQueue.parse(account_info)) + context, + spot_market.event_queue_address, + lambda account_info: SerumEventQueue.parse(account_info), + ) manager.add(subscription) latest_observer = LatestItemObserverSubscriber[EventQueue](initial) subscription.publisher.subscribe(latest_observer) @@ -267,10 +452,22 @@ def build_spot_event_queue_watcher(context: Context, manager: WebSocketSubscript return latest_observer -def build_perp_event_queue_watcher(context: Context, manager: WebSocketSubscriptionManager, health_check: HealthCheck, perp_market: PerpMarket) -> Watcher[EventQueue]: - initial: EventQueue = PerpEventQueue.load(context, perp_market.event_queue_address, perp_market.lot_size_converter) +def build_perp_event_queue_watcher( + context: Context, + manager: WebSocketSubscriptionManager, + health_check: HealthCheck, + perp_market: PerpMarket, +) -> Watcher[EventQueue]: + initial: EventQueue = PerpEventQueue.load( + context, perp_market.event_queue_address, perp_market.lot_size_converter + ) subscription = WebSocketAccountSubscription[EventQueue]( - context, perp_market.event_queue_address, lambda account_info: PerpEventQueue.parse(account_info, perp_market.lot_size_converter)) + context, + perp_market.event_queue_address, + lambda account_info: PerpEventQueue.parse( + account_info, perp_market.lot_size_converter + ), + ) manager.add(subscription) latest_observer = LatestItemObserverSubscriber[EventQueue](initial) subscription.publisher.subscribe(latest_observer) diff --git a/mango/websocketsubscription.py b/mango/websocketsubscription.py index bb78719..c339422 100644 --- a/mango/websocketsubscription.py +++ b/mango/websocketsubscription.py @@ -37,32 +37,47 @@ from .reconnectingwebsocket import ReconnectingWebsocket # -TSubscriptionInstance = typing.TypeVar('TSubscriptionInstance') +TSubscriptionInstance = typing.TypeVar("TSubscriptionInstance") -class WebSocketSubscription(Disposable, typing.Generic[TSubscriptionInstance], metaclass=abc.ABCMeta): - def __init__(self, context: Context, address: PublicKey, - constructor: typing.Callable[[AccountInfo], TSubscriptionInstance]) -> None: +class WebSocketSubscription( + Disposable, typing.Generic[TSubscriptionInstance], metaclass=abc.ABCMeta +): + def __init__( + self, + context: Context, + address: PublicKey, + constructor: typing.Callable[[AccountInfo], TSubscriptionInstance], + ) -> None: self._logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.context: Context = context self.address: PublicKey = address self.id: int = context.generate_client_id() self.subscription_id: int = 0 - self.from_account_info: typing.Callable[[AccountInfo], TSubscriptionInstance] = constructor - self.publisher: EventSource[TSubscriptionInstance] = EventSource[TSubscriptionInstance]() + self.from_account_info: typing.Callable[ + [AccountInfo], TSubscriptionInstance + ] = constructor + self.publisher: EventSource[TSubscriptionInstance] = EventSource[ + TSubscriptionInstance + ]() self.ws: typing.Optional[ReconnectingWebsocket] = None self.pong: BehaviorSubject = BehaviorSubject(datetime.now()) self._pong_subscription: typing.Optional[Disposable] = None @abc.abstractmethod def build_request(self) -> str: - raise NotImplementedError("WebSocketSubscription.build_request() is not implemented on the base type.") + raise NotImplementedError( + "WebSocketSubscription.build_request() is not implemented on the base type." + ) - def open(self,) -> None: + def open( + self, + ) -> None: websocket_url: str = self.context.client.cluster_ws_url def on_open(sock: websocket.WebSocketApp) -> None: sock.send(self.build_request()) + ws: ReconnectingWebsocket = ReconnectingWebsocket(websocket_url, on_open) ws.item.subscribe(on_next=self._on_item) # type: ignore[call-arg] ws.ping_interval = self.context.ping_interval @@ -84,7 +99,11 @@ class WebSocketSubscription(Disposable, typing.Generic[TSubscriptionInstance], m if id == self.id: subscription_id: int = int(response["result"]) self._logger.info(f"Subscription created with id {subscription_id}.") - elif (response["method"] == "accountNotification") or (response["method"] == "programNotification") or (response["method"] == "logsNotification"): + elif ( + (response["method"] == "accountNotification") + or (response["method"] == "programNotification") + or (response["method"] == "logsNotification") + ): subscription_id = response["params"]["subscription"] built = self.build_subscribed_instance(response["params"]) self.publisher.publish(built) @@ -108,49 +127,75 @@ class WebSocketSubscription(Disposable, typing.Generic[TSubscriptionInstance], m class WebSocketProgramSubscription(WebSocketSubscription[TSubscriptionInstance]): - def __init__(self, context: Context, address: PublicKey, - constructor: typing.Callable[[AccountInfo], TSubscriptionInstance]) -> None: + def __init__( + self, + context: Context, + address: PublicKey, + constructor: typing.Callable[[AccountInfo], TSubscriptionInstance], + ) -> None: super().__init__(context, address, constructor) def build_request(self) -> str: - return """ + return ( + """ { "jsonrpc": "2.0", - "id": \"""" + str(self.id) + """\", + "id": \"""" + + str(self.id) + + """\", "method": "programSubscribe", - "params": [\"""" + str(self.address) + """\", + "params": [\"""" + + str(self.address) + + """\", { "encoding": "base64", - "commitment": \"""" + str(self.context.client.commitment) + """\" + "commitment": \"""" + + str(self.context.client.commitment) + + """\" } ] } """ + ) class WebSocketAccountSubscription(WebSocketSubscription[TSubscriptionInstance]): - def __init__(self, context: Context, address: PublicKey, - constructor: typing.Callable[[AccountInfo], TSubscriptionInstance]) -> None: + def __init__( + self, + context: Context, + address: PublicKey, + constructor: typing.Callable[[AccountInfo], TSubscriptionInstance], + ) -> None: super().__init__(context, address, constructor) def build_request(self) -> str: - return """ + return ( + """ { "jsonrpc": "2.0", - "id": \"""" + str(self.id) + """\", + "id": \"""" + + str(self.id) + + """\", "method": "accountSubscribe", - "params": [\"""" + str(self.address) + """\", + "params": [\"""" + + str(self.address) + + """\", { "encoding": "base64", - "commitment": \"""" + str(self.context.client.commitment) + """\" + "commitment": \"""" + + str(self.context.client.commitment) + + """\" } ] } """ + ) class LogEvent: - def __init__(self, signatures: typing.Sequence[str], logs: typing.Sequence[str]) -> None: + def __init__( + self, signatures: typing.Sequence[str], logs: typing.Sequence[str] + ) -> None: self.signatures: typing.Sequence[str] = signatures self.logs: typing.Sequence[str] = logs @@ -176,21 +221,29 @@ class WebSocketLogSubscription(WebSocketSubscription[LogEvent]): super().__init__(context, address, lambda _: LogEvent([""], [])) def build_request(self) -> str: - return """ + return ( + """ { "jsonrpc": "2.0", - "id": \"""" + str(self.id) + """\", + "id": \"""" + + str(self.id) + + """\", "method": "logsSubscribe", "params": [ { - "mentions": [ \"""" + str(self.address) + """\" ] + "mentions": [ \"""" + + str(self.address) + + """\" ] }, { - "commitment": \"""" + str(self.context.client.commitment) + """\" + "commitment": \"""" + + str(self.context.client.commitment) + + """\" } ] } """ + ) def build(self, response: RPCResponse) -> LogEvent: return LogEvent.from_response(response) @@ -211,10 +264,14 @@ class WebSocketSubscriptionManager(Disposable, metaclass=abc.ABCMeta): self.subscriptions += [subscription] def open(self) -> None: - raise NotImplementedError("WebSocketSubscription.build_request() is not implemented on the base type.") + raise NotImplementedError( + "WebSocketSubscription.build_request() is not implemented on the base type." + ) def close(self) -> None: - raise NotImplementedError("WebSocketSubscription.build_request() is not implemented on the base type.") + raise NotImplementedError( + "WebSocketSubscription.build_request() is not implemented on the base type." + ) def on_disconnected(self, ws: websocket.WebSocketApp) -> None: for subscription in self.subscriptions: @@ -257,7 +314,9 @@ class SharedWebSocketSubscriptionManager(WebSocketSubscriptionManager): def open(self) -> None: websocket_url = self.context.client.cluster_ws_url - ws: ReconnectingWebsocket = ReconnectingWebsocket(websocket_url, self.open_handler) + ws: ReconnectingWebsocket = ReconnectingWebsocket( + websocket_url, self.open_handler + ) ws.item.subscribe(on_next=self.on_item) # type: ignore[call-arg] ws.ping_interval = self.ping_interval self.ws = ws @@ -276,23 +335,32 @@ class SharedWebSocketSubscriptionManager(WebSocketSubscriptionManager): for subscription in self.subscriptions: if subscription.id == id: self._logger.info( - f"Setting ID {subscription_id} on subscription {subscription.id} for {subscription.address}.") + f"Setting ID {subscription_id} on subscription {subscription.id} for {subscription.address}." + ) subscription.subscription_id = subscription_id return self._logger.error(f"[{self.context.name}] Subscription ID {id} not found") - def subscription_by_subscription_id(self, subscription_id: int) -> WebSocketSubscription[typing.Any]: + def subscription_by_subscription_id( + self, subscription_id: int + ) -> WebSocketSubscription[typing.Any]: for subscription in self.subscriptions: if subscription.subscription_id == subscription_id: return subscription - raise Exception(f"[{self.context.name}] No subscription with subscription ID {subscription_id} could be found.") + raise Exception( + f"[{self.context.name}] No subscription with subscription ID {subscription_id} could be found." + ) def on_item(self, response: typing.Dict[str, typing.Any]) -> None: if "method" not in response: id: int = int(response["id"]) subscription_id: int = int(response["result"]) self.add_subscription_id(id, subscription_id) - elif (response["method"] == "accountNotification") or (response["method"] == "programNotification") or (response["method"] == "logsNotification"): + elif ( + (response["method"] == "accountNotification") + or (response["method"] == "programNotification") + or (response["method"] == "logsNotification") + ): subscription_id = response["params"]["subscription"] subscription = self.subscription_by_subscription_id(subscription_id) built = subscription.build_subscribed_instance(response["params"]) diff --git a/poetry.lock b/poetry.lock index 555fd3b..c9e33c3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -27,14 +27,6 @@ python-versions = ">=3.6" examples = ["graphql-core (>=3.0.0)", "attrs", "docstring-parser", "bson", "orjson", "pydantic", "pytest", "sqlalchemy"] graphql = ["graphql-core (>=3.0.0)"] -[[package]] -name = "appdirs" -version = "1.4.4" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" -optional = false -python-versions = "*" - [[package]] name = "appnope" version = "0.1.2" @@ -79,18 +71,6 @@ docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] -[[package]] -name = "autopep8" -version = "1.6.0" -description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -pycodestyle = ">=2.8.0" -toml = "*" - [[package]] name = "backcall" version = "0.2.0" @@ -112,24 +92,24 @@ tests = ["mypy", "PyHamcrest (>=2.0.2)", "pytest (>=4.6)", "pytest-benchmark", " [[package]] name = "black" -version = "21.6b0" +version = "22.1.0" description = "The uncompromising code formatter." category = "dev" optional = false python-versions = ">=3.6.2" [package.dependencies] -appdirs = "*" -click = ">=7.1.2" +click = ">=8.0.0" mypy-extensions = ">=0.4.3" -pathspec = ">=0.8.1,<1" -regex = ">=2020.1.8" -toml = ">=0.10.1" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = ">=1.1.0" +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.6.0)", "aiohttp-cors (>=0.4.0)"] -python2 = ["typed-ast (>=1.4.2)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] [[package]] @@ -161,7 +141,7 @@ pycparser = "*" [[package]] name = "charset-normalizer" -version = "2.0.10" +version = "2.0.11" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false @@ -229,11 +209,11 @@ python-versions = ">=3.5" [[package]] name = "entrypoints" -version = "0.3" +version = "0.4" description = "Discover and load entry points from installed packages." category = "dev" optional = false -python-versions = ">=2.7" +python-versions = ">=3.6" [[package]] name = "executing" @@ -316,7 +296,7 @@ python-versions = "*" [[package]] name = "ipykernel" -version = "6.7.0" +version = "6.9.0" description = "IPython Kernel for Jupyter" category = "dev" optional = false @@ -337,7 +317,7 @@ test = ["pytest (!=5.3.4)", "pytest-cov", "flaky", "ipyparallel"] [[package]] name = "ipython" -version = "8.0.0" +version = "8.0.1" description = "IPython: Productive Interactive Computing" category = "dev" optional = false @@ -455,7 +435,7 @@ format_nongpl = ["idna", "jsonpointer (>1.13)", "webcolors", "rfc3986-validator [[package]] name = "jupyter-client" -version = "7.1.1" +version = "7.1.2" description = "Jupyter protocol implementation and client libraries" category = "dev" optional = false @@ -548,7 +528,7 @@ python-versions = ">=3.5" [[package]] name = "numpy" -version = "1.22.1" +version = "1.22.2" description = "NumPy is the fundamental package for array computing with Python." category = "main" optional = false @@ -578,28 +558,28 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "pandas" -version = "1.3.5" +version = "1.4.0" description = "Powerful data structures for data analysis, time series, and statistics" category = "main" optional = false -python-versions = ">=3.7.1" +python-versions = ">=3.8" [package.dependencies] numpy = [ - {version = ">=1.17.3", markers = "platform_machine != \"aarch64\" and platform_machine != \"arm64\" and python_version < \"3.10\""}, + {version = ">=1.18.5", markers = "platform_machine != \"aarch64\" and platform_machine != \"arm64\" and python_version < \"3.10\""}, {version = ">=1.19.2", markers = "platform_machine == \"aarch64\" and python_version < \"3.10\""}, {version = ">=1.20.0", markers = "platform_machine == \"arm64\" and python_version < \"3.10\""}, {version = ">=1.21.0", markers = "python_version >= \"3.10\""}, ] -python-dateutil = ">=2.7.3" -pytz = ">=2017.3" +python-dateutil = ">=2.8.1" +pytz = ">=2020.1" [package.extras] -test = ["hypothesis (>=3.58)", "pytest (>=6.0)", "pytest-xdist"] +test = ["hypothesis (>=5.5.3)", "pytest (>=6.0)", "pytest-xdist (>=1.31)"] [[package]] name = "pandas-stubs" -version = "1.2.0.43" +version = "1.2.0.47" description = "Type annotations for Pandas" category = "dev" optional = false @@ -644,6 +624,18 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "platformdirs" +version = "2.5.0" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] + [[package]] name = "pluggy" version = "1.0.0" @@ -658,7 +650,7 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "prompt-toolkit" -version = "3.0.24" +version = "3.0.27" description = "Library for building powerful interactive command lines in Python" category = "dev" optional = false @@ -677,7 +669,7 @@ python-versions = "*" [[package]] name = "pure-eval" -version = "0.2.1" +version = "0.2.2" description = "Safely evaluate AST nodes without side effects" category = "dev" optional = false @@ -743,7 +735,7 @@ tests = ["pytest (>=3.2.1,!=3.3.0)", "hypothesis (>=3.27.0)"] [[package]] name = "pyparsing" -version = "3.0.6" +version = "3.0.7" description = "Python parsing module" category = "dev" optional = false @@ -775,7 +767,7 @@ solana = ">=0.11.3,<1.0.0" [[package]] name = "pytest" -version = "6.2.5" +version = "7.0.0" description = "pytest: simple powerful testing with Python" category = "dev" optional = false @@ -789,10 +781,10 @@ iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" py = ">=1.8.2" -toml = "*" +tomli = ">=1.0.0" [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] [[package]] name = "python-dateutil" @@ -833,14 +825,6 @@ python-versions = ">=3.6" cffi = {version = "*", markers = "implementation_name == \"pypy\""} py = {version = "*", markers = "implementation_name == \"pypy\""} -[[package]] -name = "regex" -version = "2021.11.10" -description = "Alternative regular expression module, to replace re." -category = "dev" -optional = false -python-versions = "*" - [[package]] name = "requests" version = "2.27.1" @@ -943,17 +927,9 @@ pure-eval = "*" [package.extras] tests = ["pytest", "typeguard", "pygments", "littleutils"] -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" - [[package]] name = "tomli" -version = "2.0.0" +version = "2.0.1" description = "A lil' TOML parser" category = "dev" optional = false @@ -996,7 +972,7 @@ python-versions = "*" [[package]] name = "types-python-dateutil" -version = "2.8.7" +version = "2.8.9" description = "Typing stubs for python-dateutil" category = "dev" optional = false @@ -1004,7 +980,7 @@ python-versions = "*" [[package]] name = "types-requests" -version = "2.27.7" +version = "2.27.8" description = "Typing stubs for requests" category = "dev" optional = false @@ -1015,7 +991,7 @@ types-urllib3 = "<1.27" [[package]] name = "types-setuptools" -version = "57.4.7" +version = "57.4.9" description = "Typing stubs for setuptools" category = "dev" optional = false @@ -1031,7 +1007,7 @@ python-versions = "*" [[package]] name = "types-urllib3" -version = "1.26.7" +version = "1.26.9" description = "Typing stubs for urllib3" category = "dev" optional = false @@ -1100,7 +1076,7 @@ python-versions = ">=3.7" [[package]] name = "zstandard" -version = "0.16.0" +version = "0.17.0" description = "Zstandard bindings for Python" category = "main" optional = false @@ -1115,7 +1091,7 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.11" -content-hash = "94dc2b90ead40978fda63fd73ce7c7a5bf9018e15b91c6b2aab4cea4803b5e70" +content-hash = "7553aedbe71a7f16ed1628a13a3165b5e529f9f0947834d37946202d1fc74d1d" [metadata.files] anyio = [ @@ -1126,10 +1102,6 @@ apischema = [ {file = "apischema-0.16.6-py3-none-any.whl", hash = "sha256:b1ffcc19831fceb99c175ce53d81125bb46b8b22a019f4d8307f6d23f13e5647"}, {file = "apischema-0.16.6.tar.gz", hash = "sha256:5e53830269d17a3586103c71d7961f23df5fe8844f93afbcc3e7a1a7cbac0c75"}, ] -appdirs = [ - {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, - {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, -] appnope = [ {file = "appnope-0.1.2-py2.py3-none-any.whl", hash = "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442"}, {file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"}, @@ -1146,10 +1118,6 @@ attrs = [ {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, ] -autopep8 = [ - {file = "autopep8-1.6.0-py2.py3-none-any.whl", hash = "sha256:ed77137193bbac52d029a52c59bec1b0629b5a186c495f1eb21b126ac466083f"}, - {file = "autopep8-1.6.0.tar.gz", hash = "sha256:44f0932855039d2c15c4510d6df665e4730f2b8582704fa48f9c55bd3e17d979"}, -] backcall = [ {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, @@ -1159,8 +1127,29 @@ base58 = [ {file = "base58-2.1.1.tar.gz", hash = "sha256:c5d0cb3f5b6e81e8e35da5754388ddcc6d0d14b6c6a132cb93d69ed580a7278c"}, ] black = [ - {file = "black-21.6b0-py3-none-any.whl", hash = "sha256:dfb8c5a069012b2ab1e972e7b908f5fb42b6bbabcba0a788b86dc05067c7d9c7"}, - {file = "black-21.6b0.tar.gz", hash = "sha256:dc132348a88d103016726fe360cb9ede02cecf99b76e3660ce6c596be132ce04"}, + {file = "black-22.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1297c63b9e1b96a3d0da2d85d11cd9bf8664251fd69ddac068b98dc4f34f73b6"}, + {file = "black-22.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2ff96450d3ad9ea499fc4c60e425a1439c2120cbbc1ab959ff20f7c76ec7e866"}, + {file = "black-22.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e21e1f1efa65a50e3960edd068b6ae6d64ad6235bd8bfea116a03b21836af71"}, + {file = "black-22.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f69158a7d120fd641d1fa9a921d898e20d52e44a74a6fbbcc570a62a6bc8ab"}, + {file = "black-22.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:228b5ae2c8e3d6227e4bde5920d2fc66cc3400fde7bcc74f480cb07ef0b570d5"}, + {file = "black-22.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b1a5ed73ab4c482208d20434f700d514f66ffe2840f63a6252ecc43a9bc77e8a"}, + {file = "black-22.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35944b7100af4a985abfcaa860b06af15590deb1f392f06c8683b4381e8eeaf0"}, + {file = "black-22.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7835fee5238fc0a0baf6c9268fb816b5f5cd9b8793423a75e8cd663c48d073ba"}, + {file = "black-22.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dae63f2dbf82882fa3b2a3c49c32bffe144970a573cd68d247af6560fc493ae1"}, + {file = "black-22.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa1db02410b1924b6749c245ab38d30621564e658297484952f3d8a39fce7e8"}, + {file = "black-22.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c8226f50b8c34a14608b848dc23a46e5d08397d009446353dad45e04af0c8e28"}, + {file = "black-22.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2d6f331c02f0f40aa51a22e479c8209d37fcd520c77721c034517d44eecf5912"}, + {file = "black-22.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:742ce9af3086e5bd07e58c8feb09dbb2b047b7f566eb5f5bc63fd455814979f3"}, + {file = "black-22.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fdb8754b453fb15fad3f72cd9cad3e16776f0964d67cf30ebcbf10327a3777a3"}, + {file = "black-22.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5660feab44c2e3cb24b2419b998846cbb01c23c7fe645fee45087efa3da2d61"}, + {file = "black-22.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:6f2f01381f91c1efb1451998bd65a129b3ed6f64f79663a55fe0e9b74a5f81fd"}, + {file = "black-22.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:efbadd9b52c060a8fc3b9658744091cb33c31f830b3f074422ed27bad2b18e8f"}, + {file = "black-22.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8871fcb4b447206904932b54b567923e5be802b9b19b744fdff092bd2f3118d0"}, + {file = "black-22.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccad888050f5393f0d6029deea2a33e5ae371fd182a697313bdbd835d3edaf9c"}, + {file = "black-22.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07e5c049442d7ca1a2fc273c79d1aecbbf1bc858f62e8184abe1ad175c4f7cc2"}, + {file = "black-22.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:373922fc66676133ddc3e754e4509196a8c392fec3f5ca4486673e685a421321"}, + {file = "black-22.1.0-py3-none-any.whl", hash = "sha256:3524739d76b6b3ed1132422bf9d82123cd1705086723bc3e235ca39fd21c667d"}, + {file = "black-22.1.0.tar.gz", hash = "sha256:a7c0192d35635f6fc1174be575cb7915e92e5dd629ee79fdaf0dcfa41a80afb5"}, ] cachetools = [ {file = "cachetools-4.2.4-py3-none-any.whl", hash = "sha256:92971d3cb7d2a97efff7c7bb1657f21a8f5fb309a37530537c71b1774189f2d1"}, @@ -1223,8 +1212,8 @@ cffi = [ {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.0.10.tar.gz", hash = "sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd"}, - {file = "charset_normalizer-2.0.10-py3-none-any.whl", hash = "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455"}, + {file = "charset-normalizer-2.0.11.tar.gz", hash = "sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c"}, + {file = "charset_normalizer-2.0.11-py3-none-any.whl", hash = "sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45"}, ] click = [ {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, @@ -1269,8 +1258,8 @@ decorator = [ {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, ] entrypoints = [ - {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"}, - {file = "entrypoints-0.3.tar.gz", hash = "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"}, + {file = "entrypoints-0.4-py3-none-any.whl", hash = "sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f"}, + {file = "entrypoints-0.4.tar.gz", hash = "sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4"}, ] executing = [ {file = "executing-0.8.2-py2.py3-none-any.whl", hash = "sha256:32fc6077b103bd19e6494a72682d66d5763cf20a106d5aa7c5ccbea4e47b0df7"}, @@ -1301,12 +1290,12 @@ iniconfig = [ {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] ipykernel = [ - {file = "ipykernel-6.7.0-py3-none-any.whl", hash = "sha256:6203ccd5510ff148e9433fd4a2707c5ce8d688f026427f46e13d7ebf9b3e9787"}, - {file = "ipykernel-6.7.0.tar.gz", hash = "sha256:d82b904fdc2fd8c7b1fbe0fa481c68a11b4cd4c8ef07e6517da1f10cc3114d24"}, + {file = "ipykernel-6.9.0-py3-none-any.whl", hash = "sha256:1626b91c50e4605555ac6e5b29f1e5206d299a4a4a21483770a181be97f0f0e0"}, + {file = "ipykernel-6.9.0.tar.gz", hash = "sha256:b556e292dc6fa223f24328b1c936b9c921fafcc2f420bb0d6cfdfc42eaa90225"}, ] ipython = [ - {file = "ipython-8.0.0-py3-none-any.whl", hash = "sha256:5b58cf977635abad74d76be49dbb2e97fddd825fb8503083d55496aa1160b854"}, - {file = "ipython-8.0.0.tar.gz", hash = "sha256:004a0d05aeecd32adec4841b6e2586d5ca35785b1477db4d8333a39333e0ce98"}, + {file = "ipython-8.0.1-py3-none-any.whl", hash = "sha256:c503a0dd6ccac9c8c260b211f2dd4479c042b49636b097cc9a0d55fe62dff64c"}, + {file = "ipython-8.0.1.tar.gz", hash = "sha256:ab564d4521ea8ceaac26c3a2c6e5ddbca15c8848fd5a5cc325f960da88d42974"}, ] jedi = [ {file = "jedi-0.18.1-py2.py3-none-any.whl", hash = "sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d"}, @@ -1330,8 +1319,8 @@ jsonschema = [ {file = "jsonschema-3.2.0.tar.gz", hash = "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"}, ] jupyter-client = [ - {file = "jupyter_client-7.1.1-py3-none-any.whl", hash = "sha256:f0c576cce235c727e30b0a0da88c2755d0947d0070fa1bc45f195079ffd64e66"}, - {file = "jupyter_client-7.1.1.tar.gz", hash = "sha256:540ca35e57e83c5ece81abd9b781a57cba39a37c60a2a30c8c1b2f6663544343"}, + {file = "jupyter_client-7.1.2-py3-none-any.whl", hash = "sha256:d56f1c57bef42ff31e61b1185d3348a5b2bcde7c9a05523ae4dbe5ee0871797c"}, + {file = "jupyter_client-7.1.2.tar.gz", hash = "sha256:4ea61033726c8e579edb55626d8ee2e6bf0a83158ddf3751b8dd46b2c5cd1e96"}, ] jupyter-core = [ {file = "jupyter_core-4.9.1-py3-none-any.whl", hash = "sha256:1c091f3bbefd6f2a8782f2c1db662ca8478ac240e962ae2c66f0b87c818154ea"}, @@ -1447,28 +1436,25 @@ nest-asyncio = [ {file = "nest_asyncio-1.5.4.tar.gz", hash = "sha256:f969f6013a16fadb4adcf09d11a68a4f617c6049d7af7ac2c676110169a63abd"}, ] numpy = [ - {file = "numpy-1.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d62d6b0870b53799204515145935608cdeb4cebb95a26800b6750e48884cc5b"}, - {file = "numpy-1.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:831f2df87bd3afdfc77829bc94bd997a7c212663889d56518359c827d7113b1f"}, - {file = "numpy-1.22.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8d1563060e77096367952fb44fca595f2b2f477156de389ce7c0ade3aef29e21"}, - {file = "numpy-1.22.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69958735d5e01f7b38226a6c6e7187d72b7e4d42b6b496aca5860b611ca0c193"}, - {file = "numpy-1.22.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45a7dfbf9ed8d68fd39763940591db7637cf8817c5bce1a44f7b56c97cbe211e"}, - {file = "numpy-1.22.1-cp310-cp310-win_amd64.whl", hash = "sha256:7e957ca8112c689b728037cea9c9567c27cf912741fabda9efc2c7d33d29dfa1"}, - {file = "numpy-1.22.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:800dfeaffb2219d49377da1371d710d7952c9533b57f3d51b15e61c4269a1b5b"}, - {file = "numpy-1.22.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:65f5e257987601fdfc63f1d02fca4d1c44a2b85b802f03bd6abc2b0b14648dd2"}, - {file = "numpy-1.22.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:632e062569b0fe05654b15ef0e91a53c0a95d08ffe698b66f6ba0f927ad267c2"}, - {file = "numpy-1.22.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d245a2bf79188d3f361137608c3cd12ed79076badd743dc660750a9f3074f7c"}, - {file = "numpy-1.22.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26b4018a19d2ad9606ce9089f3d52206a41b23de5dfe8dc947d2ec49ce45d015"}, - {file = "numpy-1.22.1-cp38-cp38-win32.whl", hash = "sha256:f8ad59e6e341f38266f1549c7c2ec70ea0e3d1effb62a44e5c3dba41c55f0187"}, - {file = "numpy-1.22.1-cp38-cp38-win_amd64.whl", hash = "sha256:60f19c61b589d44fbbab8ff126640ae712e163299c2dd422bfe4edc7ec51aa9b"}, - {file = "numpy-1.22.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2db01d9838a497ba2aa9a87515aeaf458f42351d72d4e7f3b8ddbd1eba9479f2"}, - {file = "numpy-1.22.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bcd19dab43b852b03868796f533b5f5561e6c0e3048415e675bec8d2e9d286c1"}, - {file = "numpy-1.22.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:78bfbdf809fc236490e7e65715bbd98377b122f329457fffde206299e163e7f3"}, - {file = "numpy-1.22.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c51124df17f012c3b757380782ae46eee85213a3215e51477e559739f57d9bf6"}, - {file = "numpy-1.22.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88d54b7b516f0ca38a69590557814de2dd638d7d4ed04864826acaac5ebb8f01"}, - {file = "numpy-1.22.1-cp39-cp39-win32.whl", hash = "sha256:b5ec9a5eaf391761c61fd873363ef3560a3614e9b4ead17347e4deda4358bca4"}, - {file = "numpy-1.22.1-cp39-cp39-win_amd64.whl", hash = "sha256:4ac4d7c9f8ea2a79d721ebfcce81705fc3cd61a10b731354f1049eb8c99521e8"}, - {file = "numpy-1.22.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e60ef82c358ded965fdd3132b5738eade055f48067ac8a5a8ac75acc00cad31f"}, - {file = "numpy-1.22.1.zip", hash = "sha256:e348ccf5bc5235fc405ab19d53bec215bb373300e5523c7b476cc0da8a5e9973"}, + {file = "numpy-1.22.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:515a8b6edbb904594685da6e176ac9fbea8f73a5ebae947281de6613e27f1956"}, + {file = "numpy-1.22.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:76a4f9bce0278becc2da7da3b8ef854bed41a991f4226911a24a9711baad672c"}, + {file = "numpy-1.22.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:168259b1b184aa83a514f307352c25c56af111c269ffc109d9704e81f72e764b"}, + {file = "numpy-1.22.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3556c5550de40027d3121ebbb170f61bbe19eb639c7ad0c7b482cd9b560cd23b"}, + {file = "numpy-1.22.2-cp310-cp310-win_amd64.whl", hash = "sha256:aafa46b5a39a27aca566198d3312fb3bde95ce9677085efd02c86f7ef6be4ec7"}, + {file = "numpy-1.22.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:55535c7c2f61e2b2fc817c5cbe1af7cb907c7f011e46ae0a52caa4be1f19afe2"}, + {file = "numpy-1.22.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:60cb8e5933193a3cc2912ee29ca331e9c15b2da034f76159b7abc520b3d1233a"}, + {file = "numpy-1.22.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b536b6840e84c1c6a410f3a5aa727821e6108f3454d81a5cd5900999ef04f89"}, + {file = "numpy-1.22.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2638389562bda1635b564490d76713695ff497242a83d9b684d27bb4a6cc9d7a"}, + {file = "numpy-1.22.2-cp38-cp38-win32.whl", hash = "sha256:6767ad399e9327bfdbaa40871be4254d1995f4a3ca3806127f10cec778bd9896"}, + {file = "numpy-1.22.2-cp38-cp38-win_amd64.whl", hash = "sha256:03ae5850619abb34a879d5f2d4bb4dcd025d6d8fb72f5e461dae84edccfe129f"}, + {file = "numpy-1.22.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:d76a26c5118c4d96e264acc9e3242d72e1a2b92e739807b3b69d8d47684b6677"}, + {file = "numpy-1.22.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:15efb7b93806d438e3bc590ca8ef2f953b0ce4f86f337ef4559d31ec6cf9d7dd"}, + {file = "numpy-1.22.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:badca914580eb46385e7f7e4e426fea6de0a37b9e06bec252e481ae7ec287082"}, + {file = "numpy-1.22.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94dd11d9f13ea1be17bac39c1942f527cbf7065f94953cf62dfe805653da2f8f"}, + {file = "numpy-1.22.2-cp39-cp39-win32.whl", hash = "sha256:8cf33634b60c9cef346663a222d9841d3bbbc0a2f00221d6bcfd0d993d5543f6"}, + {file = "numpy-1.22.2-cp39-cp39-win_amd64.whl", hash = "sha256:59153979d60f5bfe9e4c00e401e24dfe0469ef8da6d68247439d3278f30a180f"}, + {file = "numpy-1.22.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a176959b6e7e00b5a0d6f549a479f869829bfd8150282c590deee6d099bbb6e"}, + {file = "numpy-1.22.2.zip", hash = "sha256:076aee5a3763d41da6bef9565fdf3cb987606f567cd8b104aded2b38b7b47abf"}, ] oslash = [ {file = "OSlash-0.6.3-py3-none-any.whl", hash = "sha256:89b978443b7db3ac2666106bdc3680add3c886a6d8fcdd02fd062af86d29494f"}, @@ -1479,35 +1465,31 @@ packaging = [ {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] pandas = [ - {file = "pandas-1.3.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:62d5b5ce965bae78f12c1c0df0d387899dd4211ec0bdc52822373f13a3a022b9"}, - {file = "pandas-1.3.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:adfeb11be2d54f275142c8ba9bf67acee771b7186a5745249c7d5a06c670136b"}, - {file = "pandas-1.3.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:60a8c055d58873ad81cae290d974d13dd479b82cbb975c3e1fa2cf1920715296"}, - {file = "pandas-1.3.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd541ab09e1f80a2a1760032d665f6e032d8e44055d602d65eeea6e6e85498cb"}, - {file = "pandas-1.3.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2651d75b9a167cc8cc572cf787ab512d16e316ae00ba81874b560586fa1325e0"}, - {file = "pandas-1.3.5-cp310-cp310-win_amd64.whl", hash = "sha256:aaf183a615ad790801fa3cf2fa450e5b6d23a54684fe386f7e3208f8b9bfbef6"}, - {file = "pandas-1.3.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:344295811e67f8200de2390093aeb3c8309f5648951b684d8db7eee7d1c81fb7"}, - {file = "pandas-1.3.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:552020bf83b7f9033b57cbae65589c01e7ef1544416122da0c79140c93288f56"}, - {file = "pandas-1.3.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cce0c6bbeb266b0e39e35176ee615ce3585233092f685b6a82362523e59e5b4"}, - {file = "pandas-1.3.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d28a3c65463fd0d0ba8bbb7696b23073efee0510783340a44b08f5e96ffce0c"}, - {file = "pandas-1.3.5-cp37-cp37m-win32.whl", hash = "sha256:a62949c626dd0ef7de11de34b44c6475db76995c2064e2d99c6498c3dba7fe58"}, - {file = "pandas-1.3.5-cp37-cp37m-win_amd64.whl", hash = "sha256:8025750767e138320b15ca16d70d5cdc1886e8f9cc56652d89735c016cd8aea6"}, - {file = "pandas-1.3.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fe95bae4e2d579812865db2212bb733144e34d0c6785c0685329e5b60fcb85dd"}, - {file = "pandas-1.3.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f261553a1e9c65b7a310302b9dbac31cf0049a51695c14ebe04e4bfd4a96f02"}, - {file = "pandas-1.3.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b6dbec5f3e6d5dc80dcfee250e0a2a652b3f28663492f7dab9a24416a48ac39"}, - {file = "pandas-1.3.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3bc49af96cd6285030a64779de5b3688633a07eb75c124b0747134a63f4c05f"}, - {file = "pandas-1.3.5-cp38-cp38-win32.whl", hash = "sha256:b6b87b2fb39e6383ca28e2829cddef1d9fc9e27e55ad91ca9c435572cdba51bf"}, - {file = "pandas-1.3.5-cp38-cp38-win_amd64.whl", hash = "sha256:a395692046fd8ce1edb4c6295c35184ae0c2bbe787ecbe384251da609e27edcb"}, - {file = "pandas-1.3.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bd971a3f08b745a75a86c00b97f3007c2ea175951286cdda6abe543e687e5f2f"}, - {file = "pandas-1.3.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37f06b59e5bc05711a518aa10beaec10942188dccb48918bb5ae602ccbc9f1a0"}, - {file = "pandas-1.3.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c21778a688d3712d35710501f8001cdbf96eb70a7c587a3d5613573299fdca6"}, - {file = "pandas-1.3.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3345343206546545bc26a05b4602b6a24385b5ec7c75cb6059599e3d56831da2"}, - {file = "pandas-1.3.5-cp39-cp39-win32.whl", hash = "sha256:c69406a2808ba6cf580c2255bcf260b3f214d2664a3a4197d0e640f573b46fd3"}, - {file = "pandas-1.3.5-cp39-cp39-win_amd64.whl", hash = "sha256:32e1a26d5ade11b547721a72f9bfc4bd113396947606e00d5b4a5b79b3dcb006"}, - {file = "pandas-1.3.5.tar.gz", hash = "sha256:1e4285f5de1012de20ca46b188ccf33521bff61ba5c5ebd78b4fb28e5416a9f1"}, + {file = "pandas-1.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de62cf699122dcef175988f0714678e59c453dc234c5b47b7136bfd7641e3c8c"}, + {file = "pandas-1.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:46a18572f3e1cb75db59d9461940e9ba7ee38967fa48dd58f4139197f6e32280"}, + {file = "pandas-1.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:73f7da2ccc38cc988b74e5400b430b7905db5f2c413ff215506bea034eaf832d"}, + {file = "pandas-1.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5229c95db3a907451dacebc551492db6f7d01743e49bbc862f4a6010c227d187"}, + {file = "pandas-1.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe454180ad31bbbe1e5d111b44443258730467f035e26b4e354655ab59405871"}, + {file = "pandas-1.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:784cca3f69cfd7f6bd7c7fdb44f2bbab17e6de55725e9ff36d6f382510dfefb5"}, + {file = "pandas-1.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:de8f8999864399529e8514a2e6bfe00fd161f0a667903655552ed12e583ae3cb"}, + {file = "pandas-1.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0f19504f2783526fb5b4de675ea69d68974e21c1624f4b92295d057a31d5ec5f"}, + {file = "pandas-1.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f045bb5c6bfaba536089573bf97d6b8ccc7159d951fe63904c395a5e486fbe14"}, + {file = "pandas-1.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5280d057ddae06fe4a3cd6aa79040b8c205cd6dd21743004cf8635f39ed01712"}, + {file = "pandas-1.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f3b74335390dda49f5d5089fab71958812bf56f42aa27663ee4c16d19f4f1c5"}, + {file = "pandas-1.4.0-cp38-cp38-win32.whl", hash = "sha256:51e5da3802aaee1aa4254108ffaf1129a15fb3810b7ce8da1ec217c655b418f5"}, + {file = "pandas-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:f103a5cdcd66cb18882ccdc18a130c31c3cfe3529732e7f10a8ab3559164819c"}, + {file = "pandas-1.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4a8d5a200f8685e7ea562b2f022c77ab7cb82c1ca5b240e6965faa6f84e5c1e9"}, + {file = "pandas-1.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b5af258c7b090cca7b742cf2bd67ad1919aa9e4e681007366c9edad2d6a3d42b"}, + {file = "pandas-1.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:156aac90dd7b303bf0b91bae96c0503212777f86c731e41929c571125d26c8e9"}, + {file = "pandas-1.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dad075089e17a72391de33021ad93720aff258c3c4b68c78e1cafce7e447045"}, + {file = "pandas-1.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d59c958d6b8f96fdf850c7821571782168d5acfe75ccf78cd8d1ac15fb921df"}, + {file = "pandas-1.4.0-cp39-cp39-win32.whl", hash = "sha256:55ec0e192eefa26d823fc25a1f213d6c304a3592915f368e360652994cdb8d9a"}, + {file = "pandas-1.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:23c04dab11f3c6359cfa7afa83d3d054a8f8c283d773451184d98119ef54da97"}, + {file = "pandas-1.4.0.tar.gz", hash = "sha256:cdd76254c7f0a1583bd4e4781fb450d0ebf392e10d3f12e92c95575942e37df5"}, ] pandas-stubs = [ - {file = "pandas-stubs-1.2.0.43.tar.gz", hash = "sha256:0c47c213ab92a8608376eee00efd207e7f7af0a7b44342a6dbaf61a6b21bcc0d"}, - {file = "pandas_stubs-1.2.0.43-py3-none-any.whl", hash = "sha256:75c153b06b87bdb663908d1dc382abeb898e0e777c165cbb620df839a609370e"}, + {file = "pandas-stubs-1.2.0.47.tar.gz", hash = "sha256:78801efb504abe75c2cf73cb3469b97003080e80fb03aa2a406ca4928f3fd887"}, + {file = "pandas_stubs-1.2.0.47-py3-none-any.whl", hash = "sha256:54df4b9d344f2bbb5b65646a77d420b6f0250d2bb6742e5b04e65006acc60e8a"}, ] parso = [ {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, @@ -1525,21 +1507,25 @@ pickleshare = [ {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, ] +platformdirs = [ + {file = "platformdirs-2.5.0-py3-none-any.whl", hash = "sha256:30671902352e97b1eafd74ade8e4a694782bd3471685e78c32d0fdfd3aa7e7bb"}, + {file = "platformdirs-2.5.0.tar.gz", hash = "sha256:8ec11dfba28ecc0715eb5fb0147a87b1bf325f349f3da9aab2cd6b50b96b692b"}, +] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] prompt-toolkit = [ - {file = "prompt_toolkit-3.0.24-py3-none-any.whl", hash = "sha256:e56f2ff799bacecd3e88165b1e2f5ebf9bcd59e80e06d395fa0cc4b8bd7bb506"}, - {file = "prompt_toolkit-3.0.24.tar.gz", hash = "sha256:1bb05628c7d87b645974a1bad3f17612be0c29fa39af9f7688030163f680bad6"}, + {file = "prompt_toolkit-3.0.27-py3-none-any.whl", hash = "sha256:cb7dae7d2c59188c85a1d6c944fad19aded6a26bd9c8ae115a4e1c20eb90b713"}, + {file = "prompt_toolkit-3.0.27.tar.gz", hash = "sha256:f2b6a8067a4fb959d3677d1ed764cc4e63e0f6f565b9a4fc7edc2b18bf80217b"}, ] ptyprocess = [ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, ] pure-eval = [ - {file = "pure_eval-0.2.1-py3-none-any.whl", hash = "sha256:94eeb505a88721bec7bb21a4ac49758b8b1a01530da1a70d4ffc1d9937689d71"}, - {file = "pure_eval-0.2.1.tar.gz", hash = "sha256:0f04483b16c9429532d2c0ddc96e2b3bb6b2dc37a2bfb0e986248dbfd0b78873"}, + {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, + {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, ] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, @@ -1574,8 +1560,8 @@ pynacl = [ {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, ] pyparsing = [ - {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, - {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, + {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, + {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, ] pyrsistent = [ {file = "pyrsistent-0.18.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:df46c854f490f81210870e509818b729db4488e1f30f2a1ce1698b2295a878d1"}, @@ -1605,8 +1591,8 @@ pyserum = [ {file = "pyserum-0.5.0a0.tar.gz", hash = "sha256:0e13a479c839a596095bcd87fd348b90f9020fa5b1d65bbad17e7daf8086fa9c"}, ] pytest = [ - {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, - {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, + {file = "pytest-7.0.0-py3-none-any.whl", hash = "sha256:42901e6bd4bd4a0e533358a86e848427a49005a3256f657c5c8f8dd35ef137a9"}, + {file = "pytest-7.0.0.tar.gz", hash = "sha256:dad48ffda394e5ad9aa3b7d7ddf339ed502e5e365b1350e0af65f4a602344b11"}, ] python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, @@ -1679,82 +1665,6 @@ pyzmq = [ {file = "pyzmq-22.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:d6157793719de168b199194f6b6173f0ccd3bf3499e6870fac17086072e39115"}, {file = "pyzmq-22.3.0.tar.gz", hash = "sha256:8eddc033e716f8c91c6a2112f0a8ebc5e00532b4a6ae1eb0ccc48e027f9c671c"}, ] -regex = [ - {file = "regex-2021.11.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9345b6f7ee578bad8e475129ed40123d265464c4cfead6c261fd60fc9de00bcf"}, - {file = "regex-2021.11.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:416c5f1a188c91e3eb41e9c8787288e707f7d2ebe66e0a6563af280d9b68478f"}, - {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0538c43565ee6e703d3a7c3bdfe4037a5209250e8502c98f20fea6f5fdf2965"}, - {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee1227cf08b6716c85504aebc49ac827eb88fcc6e51564f010f11a406c0a667"}, - {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6650f16365f1924d6014d2ea770bde8555b4a39dc9576abb95e3cd1ff0263b36"}, - {file = "regex-2021.11.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30ab804ea73972049b7a2a5c62d97687d69b5a60a67adca07eb73a0ddbc9e29f"}, - {file = "regex-2021.11.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68a067c11463de2a37157930d8b153005085e42bcb7ad9ca562d77ba7d1404e0"}, - {file = "regex-2021.11.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:162abfd74e88001d20cb73ceaffbfe601469923e875caf9118333b1a4aaafdc4"}, - {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9ed0b1e5e0759d6b7f8e2f143894b2a7f3edd313f38cf44e1e15d360e11749b"}, - {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:473e67837f786404570eae33c3b64a4b9635ae9f00145250851a1292f484c063"}, - {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2fee3ed82a011184807d2127f1733b4f6b2ff6ec7151d83ef3477f3b96a13d03"}, - {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:d5fd67df77bab0d3f4ea1d7afca9ef15c2ee35dfb348c7b57ffb9782a6e4db6e"}, - {file = "regex-2021.11.10-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5d408a642a5484b9b4d11dea15a489ea0928c7e410c7525cd892f4d04f2f617b"}, - {file = "regex-2021.11.10-cp310-cp310-win32.whl", hash = "sha256:98ba568e8ae26beb726aeea2273053c717641933836568c2a0278a84987b2a1a"}, - {file = "regex-2021.11.10-cp310-cp310-win_amd64.whl", hash = "sha256:780b48456a0f0ba4d390e8b5f7c661fdd218934388cde1a974010a965e200e12"}, - {file = "regex-2021.11.10-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:dba70f30fd81f8ce6d32ddeef37d91c8948e5d5a4c63242d16a2b2df8143aafc"}, - {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1f54b9b4b6c53369f40028d2dd07a8c374583417ee6ec0ea304e710a20f80a0"}, - {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fbb9dc00e39f3e6c0ef48edee202f9520dafb233e8b51b06b8428cfcb92abd30"}, - {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666abff54e474d28ff42756d94544cdfd42e2ee97065857413b72e8a2d6a6345"}, - {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5537f71b6d646f7f5f340562ec4c77b6e1c915f8baae822ea0b7e46c1f09b733"}, - {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2e07c6a26ed4bea91b897ee2b0835c21716d9a469a96c3e878dc5f8c55bb23"}, - {file = "regex-2021.11.10-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ca5f18a75e1256ce07494e245cdb146f5a9267d3c702ebf9b65c7f8bd843431e"}, - {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:74cbeac0451f27d4f50e6e8a8f3a52ca074b5e2da9f7b505c4201a57a8ed6286"}, - {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:3598893bde43091ee5ca0a6ad20f08a0435e93a69255eeb5f81b85e81e329264"}, - {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:50a7ddf3d131dc5633dccdb51417e2d1910d25cbcf842115a3a5893509140a3a"}, - {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:61600a7ca4bcf78a96a68a27c2ae9389763b5b94b63943d5158f2a377e09d29a"}, - {file = "regex-2021.11.10-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:563d5f9354e15e048465061509403f68424fef37d5add3064038c2511c8f5e00"}, - {file = "regex-2021.11.10-cp36-cp36m-win32.whl", hash = "sha256:93a5051fcf5fad72de73b96f07d30bc29665697fb8ecdfbc474f3452c78adcf4"}, - {file = "regex-2021.11.10-cp36-cp36m-win_amd64.whl", hash = "sha256:b483c9d00a565633c87abd0aaf27eb5016de23fed952e054ecc19ce32f6a9e7e"}, - {file = "regex-2021.11.10-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fff55f3ce50a3ff63ec8e2a8d3dd924f1941b250b0aac3d3d42b687eeff07a8e"}, - {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e32d2a2b02ccbef10145df9135751abea1f9f076e67a4e261b05f24b94219e36"}, - {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53db2c6be8a2710b359bfd3d3aa17ba38f8aa72a82309a12ae99d3c0c3dcd74d"}, - {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2207ae4f64ad3af399e2d30dde66f0b36ae5c3129b52885f1bffc2f05ec505c8"}, - {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5ca078bb666c4a9d1287a379fe617a6dccd18c3e8a7e6c7e1eb8974330c626a"}, - {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd33eb9bdcfbabab3459c9ee651d94c842bc8a05fabc95edf4ee0c15a072495e"}, - {file = "regex-2021.11.10-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05b7d6d7e64efe309972adab77fc2af8907bb93217ec60aa9fe12a0dad35874f"}, - {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:42b50fa6666b0d50c30a990527127334d6b96dd969011e843e726a64011485da"}, - {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6e1d2cc79e8dae442b3fa4a26c5794428b98f81389af90623ffcc650ce9f6732"}, - {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:0416f7399e918c4b0e074a0f66e5191077ee2ca32a0f99d4c187a62beb47aa05"}, - {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:ce298e3d0c65bd03fa65ffcc6db0e2b578e8f626d468db64fdf8457731052942"}, - {file = "regex-2021.11.10-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dc07f021ee80510f3cd3af2cad5b6a3b3a10b057521d9e6aaeb621730d320c5a"}, - {file = "regex-2021.11.10-cp37-cp37m-win32.whl", hash = "sha256:e71255ba42567d34a13c03968736c5d39bb4a97ce98188fafb27ce981115beec"}, - {file = "regex-2021.11.10-cp37-cp37m-win_amd64.whl", hash = "sha256:07856afef5ffcc052e7eccf3213317fbb94e4a5cd8177a2caa69c980657b3cb4"}, - {file = "regex-2021.11.10-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ba05430e819e58544e840a68b03b28b6d328aff2e41579037e8bab7653b37d83"}, - {file = "regex-2021.11.10-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7f301b11b9d214f83ddaf689181051e7f48905568b0c7017c04c06dfd065e244"}, - {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aaa4e0705ef2b73dd8e36eeb4c868f80f8393f5f4d855e94025ce7ad8525f50"}, - {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:788aef3549f1924d5c38263104dae7395bf020a42776d5ec5ea2b0d3d85d6646"}, - {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f8af619e3be812a2059b212064ea7a640aff0568d972cd1b9e920837469eb3cb"}, - {file = "regex-2021.11.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85bfa6a5413be0ee6c5c4a663668a2cad2cbecdee367630d097d7823041bdeec"}, - {file = "regex-2021.11.10-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f23222527b307970e383433daec128d769ff778d9b29343fb3496472dc20dabe"}, - {file = "regex-2021.11.10-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:da1a90c1ddb7531b1d5ff1e171b4ee61f6345119be7351104b67ff413843fe94"}, - {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f5be7805e53dafe94d295399cfbe5227f39995a997f4fd8539bf3cbdc8f47ca8"}, - {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a955b747d620a50408b7fdf948e04359d6e762ff8a85f5775d907ceced715129"}, - {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:139a23d1f5d30db2cc6c7fd9c6d6497872a672db22c4ae1910be22d4f4b2068a"}, - {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:ca49e1ab99593438b204e00f3970e7a5f70d045267051dfa6b5f4304fcfa1dbf"}, - {file = "regex-2021.11.10-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:96fc32c16ea6d60d3ca7f63397bff5c75c5a562f7db6dec7d412f7c4d2e78ec0"}, - {file = "regex-2021.11.10-cp38-cp38-win32.whl", hash = "sha256:0617383e2fe465732af4509e61648b77cbe3aee68b6ac8c0b6fe934db90be5cc"}, - {file = "regex-2021.11.10-cp38-cp38-win_amd64.whl", hash = "sha256:a3feefd5e95871872673b08636f96b61ebef62971eab044f5124fb4dea39919d"}, - {file = "regex-2021.11.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f7f325be2804246a75a4f45c72d4ce80d2443ab815063cdf70ee8fb2ca59ee1b"}, - {file = "regex-2021.11.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:537ca6a3586931b16a85ac38c08cc48f10fc870a5b25e51794c74df843e9966d"}, - {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eef2afb0fd1747f33f1ee3e209bce1ed582d1896b240ccc5e2697e3275f037c7"}, - {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:432bd15d40ed835a51617521d60d0125867f7b88acf653e4ed994a1f8e4995dc"}, - {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b43c2b8a330a490daaef5a47ab114935002b13b3f9dc5da56d5322ff218eeadb"}, - {file = "regex-2021.11.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:962b9a917dd7ceacbe5cd424556914cb0d636001e393b43dc886ba31d2a1e449"}, - {file = "regex-2021.11.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fa8c626d6441e2d04b6ee703ef2d1e17608ad44c7cb75258c09dd42bacdfc64b"}, - {file = "regex-2021.11.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3c5fb32cc6077abad3bbf0323067636d93307c9fa93e072771cf9a64d1c0f3ef"}, - {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cd410a1cbb2d297c67d8521759ab2ee3f1d66206d2e4328502a487589a2cb21b"}, - {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e6096b0688e6e14af6a1b10eaad86b4ff17935c49aa774eac7c95a57a4e8c296"}, - {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:529801a0d58809b60b3531ee804d3e3be4b412c94b5d267daa3de7fadef00f49"}, - {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0f594b96fe2e0821d026365f72ac7b4f0b487487fb3d4aaf10dd9d97d88a9737"}, - {file = "regex-2021.11.10-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2409b5c9cef7054dde93a9803156b411b677affc84fca69e908b1cb2c540025d"}, - {file = "regex-2021.11.10-cp39-cp39-win32.whl", hash = "sha256:3b5df18db1fccd66de15aa59c41e4f853b5df7550723d26aa6cb7f40e5d9da5a"}, - {file = "regex-2021.11.10-cp39-cp39-win_amd64.whl", hash = "sha256:83ee89483672b11f8952b158640d0c0ff02dc43d9cb1b70c1564b49abe92ce29"}, - {file = "regex-2021.11.10.tar.gz", hash = "sha256:f341ee2df0999bfdf7a95e448075effe0db212a59387de1a70690e4acb03d4c6"}, -] requests = [ {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, @@ -1786,13 +1696,9 @@ stack-data = [ {file = "stack_data-0.1.4-py3-none-any.whl", hash = "sha256:02cc0683cbc445ae4ca8c4e3a0e58cb1df59f252efb0aa016b34804a707cf9bc"}, {file = "stack_data-0.1.4.tar.gz", hash = "sha256:7769ed2482ce0030e00175dd1bf4ef1e873603b6ab61cd3da443b410e64e9477"}, ] -toml = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] tomli = [ - {file = "tomli-2.0.0-py3-none-any.whl", hash = "sha256:b5bde28da1fed24b9bd1d4d2b8cba62300bfb4ec9a6187a957e8ddb9434c5224"}, - {file = "tomli-2.0.0.tar.gz", hash = "sha256:c292c34f58502a1eb2bbb9f5bbc9a5ebc37bee10ffb8c2d6bbdfa8eb13cc14e1"}, + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] tornado = [ {file = "tornado-6.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32"}, @@ -1850,24 +1756,24 @@ types-certifi = [ {file = "types_certifi-2021.10.8.1-py3-none-any.whl", hash = "sha256:2290008f32e6ac7c69e779d04fa1bc4c6bb4c7200aa3b3b072ad5475a8968aa5"}, ] types-python-dateutil = [ - {file = "types-python-dateutil-2.8.7.tar.gz", hash = "sha256:f34bae3c8e83c67f7cbf407fc9d6ee889d7708b42270a496b6026a5537e80482"}, - {file = "types_python_dateutil-2.8.7-py3-none-any.whl", hash = "sha256:0e3b96fc79d3e58e011a9446ec85be080e2d2a8840dfe124ce30f4e53dbca9e5"}, + {file = "types-python-dateutil-2.8.9.tar.gz", hash = "sha256:90f95a6b6d4faba359287f17a2cae511ccc9d4abc89b01969bdac1185815c05d"}, + {file = "types_python_dateutil-2.8.9-py3-none-any.whl", hash = "sha256:d60db7f5d40ce85ce54e7fb14e4157daf33e24f5a4bfb5f44ee7a5b790dfabd0"}, ] types-requests = [ - {file = "types-requests-2.27.7.tar.gz", hash = "sha256:f38bd488528cdcbce5b01dc953972f3cead0d060cfd9ee35b363066c25bab13c"}, - {file = "types_requests-2.27.7-py3-none-any.whl", hash = "sha256:2e0e100dd489f83870d4f61949d3a7eae4821e7bfbf46c57e463c38f92d473d4"}, + {file = "types-requests-2.27.8.tar.gz", hash = "sha256:c2f4e4754d07ca0a88fd8a89bbc6c8a9f90fb441f9c9b572fd5c484f04817486"}, + {file = "types_requests-2.27.8-py3-none-any.whl", hash = "sha256:8ec9f5f84adc6f579f53943312c28a84e87dc70201b54f7c4fbc7d22ecfa8a3e"}, ] types-setuptools = [ - {file = "types-setuptools-57.4.7.tar.gz", hash = "sha256:9677d969b00ec1c14552f5be2b2b47a6fbea4d0ed4de0fdcee18abdaa0cc9267"}, - {file = "types_setuptools-57.4.7-py3-none-any.whl", hash = "sha256:ffda504687ea02d4b7751c0d1df517fbbcdc276836d90849e4f1a5f1ccd79f01"}, + {file = "types-setuptools-57.4.9.tar.gz", hash = "sha256:536ef74744f8e1e4be4fc719887f886e74e4cf3c792b4a06984320be4df450b5"}, + {file = "types_setuptools-57.4.9-py3-none-any.whl", hash = "sha256:948dc6863373750e2cd0b223a84f1fb608414cde5e55cf38ea657b93aeb411d2"}, ] types-toml = [ {file = "types-toml-0.10.3.tar.gz", hash = "sha256:215a7a79198651ec5bdfd66193c1e71eb681a42f3ef7226c9af3123ced62564a"}, {file = "types_toml-0.10.3-py3-none-any.whl", hash = "sha256:988457744d9774d194e3539388772e3a685d8057b7c4a89407afeb0a6cbd1b14"}, ] types-urllib3 = [ - {file = "types-urllib3-1.26.7.tar.gz", hash = "sha256:cfd1fbbe4ba9a605ed148294008aac8a7b8b7472651d1cc357d507ae5962e3d2"}, - {file = "types_urllib3-1.26.7-py3-none-any.whl", hash = "sha256:3adcf2cb5981809091dbff456e6999fe55f201652d8c360f99997de5ac2f556e"}, + {file = "types-urllib3-1.26.9.tar.gz", hash = "sha256:abd2d4857837482b1834b4817f0587678dcc531dbc9abe4cde4da28cef3f522c"}, + {file = "types_urllib3-1.26.9-py3-none-any.whl", hash = "sha256:4a54f6274ab1c80968115634a55fb9341a699492b95e32104a7c513db9fe02e9"}, ] typing-extensions = [ {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, @@ -1940,48 +1846,48 @@ websockets = [ {file = "websockets-10.1.tar.gz", hash = "sha256:181d2b25de5a437b36aefedaf006ecb6fa3aa1328ec0236cdde15f32f9d3ff6d"}, ] zstandard = [ - {file = "zstandard-0.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eba125d3899f2003debf97019cd6f46f841a405df067da23d11443ad17952a40"}, - {file = "zstandard-0.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:57a6cfc34d906d514358769ed6d510b312be1cf033aafb5db44865a6717579bd"}, - {file = "zstandard-0.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bdda52224043e13ed20f847e3b308de1c9372d1563824fad776b1cf1f847ef0"}, - {file = "zstandard-0.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c8c0e813b67de1c9d7f2760768c4ae53f011c75ace18d5cff4fb40d2173763f"}, - {file = "zstandard-0.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b61586b0ff55c4137e512f1e9df4e4d7a6e1e9df782b4b87652df27737c90cc1"}, - {file = "zstandard-0.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae19628886d994ac1f3d2fc7f9ed5bb551d81000f7b4e0c57a0e88301aea2766"}, - {file = "zstandard-0.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4d8a296dab7f8f5d53acc693a6785751f43ca39b51c8eabc672f978306fb40e6"}, - {file = "zstandard-0.16.0-cp310-cp310-win32.whl", hash = "sha256:87bea44ad24c15cd872263c0d5f912186a4be3db361eab3b25f1a61dcb5ca014"}, - {file = "zstandard-0.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:c75557d53bb2d064521ff20cce9b8a51ee8301e031b1d6bcedb6458dda3bc85d"}, - {file = "zstandard-0.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8f5785c0b9b71d49d789240ae16a636728596631cf100f32b963a6f9857af5a4"}, - {file = "zstandard-0.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef759c1dfe78aa5a01747d3465d2585de14e08fc2b0195ce3f31f45477fc5a72"}, - {file = "zstandard-0.16.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd5a2287893e52204e4ce9d0e1bcea6240661dbb412efb53d5446b881d3c10a2"}, - {file = "zstandard-0.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8a745862ed525eee4e28bdbd58bf3ea952bf9da3c31bb4e4ce11ef15aea5c625"}, - {file = "zstandard-0.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce61492764d0442ca1e81d38d7bf7847d7df5003bce28089bab64c0519749351"}, - {file = "zstandard-0.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ac5d97f9dece91a1162f651da79b735c5cde4d5863477785962aad648b592446"}, - {file = "zstandard-0.16.0-cp36-cp36m-win32.whl", hash = "sha256:91efd5ea5fb3c347e7ebb6d5622bfa37d72594a2dec37c5dde70b691edb6cc03"}, - {file = "zstandard-0.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:9bcbfe1ec89789239f63daeea8778488cb5ba9034a374d7753815935f83dad65"}, - {file = "zstandard-0.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b46220bef7bf9271a2a05512e86acbabc86cca08bebde8447bdbb4acb3179447"}, - {file = "zstandard-0.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b760fc8118b1a0aa1d8f4e2012622e8f5f178d4b8cb94f8c6d2948b6a49a485"}, - {file = "zstandard-0.16.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08a728715858f1477239887ba3c692bc462b2c86e7a8e467dc5affa7bba9093f"}, - {file = "zstandard-0.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e9456492eb13249841e53221e742bef93f4868122bfc26bafa12a07677619732"}, - {file = "zstandard-0.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74cbea966462afed5a89eb99e4577538d10d425e05bf6240a75c086d59ccaf89"}, - {file = "zstandard-0.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:127c4c93f578d9b509732c74ed9b44b23e94041ba11b13827be0a7d2e3869b39"}, - {file = "zstandard-0.16.0-cp37-cp37m-win32.whl", hash = "sha256:c7e6b6ad58ae6f77872da9376ef0ecbf8c1ae7a0c8fc29a2473abc90f79a9a1b"}, - {file = "zstandard-0.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2e31680d1bcf85e7a58a45df7365af894402ae77a9868c751dc991dd13099a5f"}, - {file = "zstandard-0.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8d5fe983e23b05f0e924fe8d0dd3935f0c9fd3266e4c6ff8621c12c350da299d"}, - {file = "zstandard-0.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:42992e89b250fe6878c175119af529775d4be7967cd9de86990145d615d6a444"}, - {file = "zstandard-0.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d40447f4a44b442fa6715779ff49a1e319729d829198279927d18bca0d7ac32d"}, - {file = "zstandard-0.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffe1d24c5e11e98e4c5f96f846cdd19619d8c7e5e8e5082bed62d39baa30cecb"}, - {file = "zstandard-0.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:11216b47c62e9fc71a25f4b42f525a81da268071bdb434bc1e642ffc38a24a02"}, - {file = "zstandard-0.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2ea1937eff0ed5621876dc377933fe76624abfb2ab5b418995f43af6bac50de"}, - {file = "zstandard-0.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d9946cfe54bf3365f14a5aa233eb2425de3b77eac6a4c7d03dda7dbb6acd3267"}, - {file = "zstandard-0.16.0-cp38-cp38-win32.whl", hash = "sha256:6ed51162e270b9b8097dcae6f2c239ada05ec112194633193ec3241498988924"}, - {file = "zstandard-0.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:066488e721ec882485a500c216302b443f2eaef39356f7c65130e76c671e3ce2"}, - {file = "zstandard-0.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cae9bfcb9148152f8bfb9163b4b779326ca39fe9889e45e0572c56d25d5021be"}, - {file = "zstandard-0.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:92e6c1a656390176d51125847f2f422f9d8ed468c24b63958f6ee50d9aa98c83"}, - {file = "zstandard-0.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9ec6de2c058e611e9dfe88d9809a5676bc1d2a53543c1273a90a60e41b8f43c"}, - {file = "zstandard-0.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a92aa26789f17ca3b1f45cc7e728597165e2b166b99d1204bb397a672edee761"}, - {file = "zstandard-0.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:12dddee2574b00c262270cfb46bd0c048e92208b95fdd39ad2a9eac1cef30498"}, - {file = "zstandard-0.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8828f4e78774a6c0b8d21e59677f8f48d2e17fe2ef72793c94c10abc032c41c"}, - {file = "zstandard-0.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5251ac352d8350869c404a0ca94457da018b726f692f6456ec82bbf907fbc956"}, - {file = "zstandard-0.16.0-cp39-cp39-win32.whl", hash = "sha256:453e42af96923582ddbf3acf843f55d2dc534a3f7b345003852dd522aa51eae6"}, - {file = "zstandard-0.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:be68fbac1e88f0dbe033a2d2e3aaaf9c8307730b905f3cd3c698ca4b904f0702"}, - {file = "zstandard-0.16.0.tar.gz", hash = "sha256:eaae2d3e8fdf8bfe269628385087e4b648beef85bb0c187644e7df4fb0fe9046"}, + {file = "zstandard-0.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a1991cdf2e81e643b53fb8d272931d2bdf5f4e70d56a457e1ef95bde147ae627"}, + {file = "zstandard-0.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4768449d8d1b0785309ace288e017cc5fa42e11a52bf08c90d9c3eb3a7a73cc6"}, + {file = "zstandard-0.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ad6d2952b41d9a0ea702a474cc08c05210c6289e29dd496935c9ca3c7fb45c"}, + {file = "zstandard-0.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90a9ba3a9c16b86afcb785b3c9418af39ccfb238fd5f6e429166e3ca8542b01f"}, + {file = "zstandard-0.17.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9cf18c156b3a108197a8bf90b37d03c31c8ef35a7c18807b321d96b74e12c301"}, + {file = "zstandard-0.17.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c81fd9386449df0ebf1ab3e01187bb30d61122c74df53ba4880a2454d866e55d"}, + {file = "zstandard-0.17.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:787efc741e61e00ffe5e65dac99b0dc5c88b9421012a207a91b869a8b1164921"}, + {file = "zstandard-0.17.0-cp310-cp310-win32.whl", hash = "sha256:49cd09ccbd1e3c0e2690dd62ebf95064d84aa42b9db381867e0b138631f969f2"}, + {file = "zstandard-0.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:d78aac2ffc4e88ab1cbcad844669924c24e24c7c255de9628a18f14d832007c5"}, + {file = "zstandard-0.17.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c19d1e06569c277dcc872d80cbadf14a29e8199e013ff2a176d169f461439a40"}, + {file = "zstandard-0.17.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d916018289d2f9a882e90d2e3bd41652861ce11b5ecd8515fa07ad31d97d56e5"}, + {file = "zstandard-0.17.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0c87f097d6867833a839b086eb8d03676bb87c2efa067a131099f04aa790683"}, + {file = "zstandard-0.17.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:60943f71e3117583655a1eb76188a7cc78a25267ef09cc74be4d25a0b0c8b947"}, + {file = "zstandard-0.17.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:208fa6bead577b2607205640078ee452e81fe20fe96321623c632bad9ebd7148"}, + {file = "zstandard-0.17.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:42f3c02c7021073cafbc6cd152b288c56a25e585518861589bb08b063b6d2ad2"}, + {file = "zstandard-0.17.0-cp36-cp36m-win32.whl", hash = "sha256:2a2ac752162ba5cbc869c60c4a4e54e890b2ee2ffb57d3ff159feab1ae4518db"}, + {file = "zstandard-0.17.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d1405caa964ba11b2396bd9fd19940440217345752e192c936d084ba5fe67dcb"}, + {file = "zstandard-0.17.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ef62eb3bcfd6d786f439828bb544ebd3936432db669403e0b8f48e424f1d55f1"}, + {file = "zstandard-0.17.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:477f172807a9fa83467b30d7c58876af1410d20177c554c27525211edf535bae"}, + {file = "zstandard-0.17.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de1aa618306a741e0497878b7f845fd6c397e52dd096fb76ed791e7268887176"}, + {file = "zstandard-0.17.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a827b9c464ee966524f8e82ec1aabb4a77ff9514cae041667fa81ae2ec8bd3e9"}, + {file = "zstandard-0.17.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cf96ace804945e53bc3e5294097e5fa32a2d43bc52416c632b414b870ee0a21"}, + {file = "zstandard-0.17.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:802109f67328c5b822d4fdac28e1cf65a24de2e2e99d76cdbeee9121cedb1b6c"}, + {file = "zstandard-0.17.0-cp37-cp37m-win32.whl", hash = "sha256:a628f20d019feb0f3a171c7a55cc4f75681f3b8c1bd7a5009165a487314887cd"}, + {file = "zstandard-0.17.0-cp37-cp37m-win_amd64.whl", hash = "sha256:7d2e7abac41d2b4b18f03575aca860d2cb647c343e13c23d6c769106a3db2f6f"}, + {file = "zstandard-0.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f502fe79757434292174b04db114f9e25c767b2d5ca9e759d118b22a66f445f8"}, + {file = "zstandard-0.17.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e37c4e21f696d6bcdbbc7caf98dffa505d04c0053909b9db0a6e8ca3b935eb07"}, + {file = "zstandard-0.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fd386d0ec1f9343f1776391d9e60d4eedced0a0b0e625bb89b91f6d05f70e83"}, + {file = "zstandard-0.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91a228a077fc7cd8486c273788d4a006a37d060cb4293f471eb0325c3113af68"}, + {file = "zstandard-0.17.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:59eadb9f347d40e8f7ef77caffd0c04a31e82c1df82fe2d2a688032429d750ac"}, + {file = "zstandard-0.17.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a71809ec062c5b7acf286ba6d4484e6fe8130fc2b93c25e596bb34e7810c79b2"}, + {file = "zstandard-0.17.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8aedd38d357f6d5e2facd88ce62b4976afdc29db57216a23f14a0cd0ca05a8a3"}, + {file = "zstandard-0.17.0-cp38-cp38-win32.whl", hash = "sha256:bd842ae3dbb7cba88beb022161c819fa80ca7d0c5a4ddd209e7daae85d904e49"}, + {file = "zstandard-0.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:d0e9fec68e304fb35c559c44530213adbc7d5918bdab906a45a0f40cd56c4de2"}, + {file = "zstandard-0.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9ec62a4c2dbb0a86ee5138c16ef133e59a23ac108f8d7ac97aeb61d410ce6857"}, + {file = "zstandard-0.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d5373a56b90052f171c8634fedc53a6ac371e6c742606e9825772a394bdbd4b0"}, + {file = "zstandard-0.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2e3ea5e4d5ecf3faefd4a5294acb6af1f0578b0cdd75d6b4529c45deaa54d6f"}, + {file = "zstandard-0.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a3a1aa9528087f6f4c47f4ece2d5e6a160527821263fb8174ff36429233e093"}, + {file = "zstandard-0.17.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bdf691a205bc492956e6daef7a06fb38f8cbe8b2c1cb0386f35f4412c360c9e9"}, + {file = "zstandard-0.17.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db993a56e21d903893933887984ca9b0d274f2b1db7b3cf21ba129783953864f"}, + {file = "zstandard-0.17.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a7756a9446f83c81101f6c0a48c3bfd8d387a249933c57b0d095ca8b20541337"}, + {file = "zstandard-0.17.0-cp39-cp39-win32.whl", hash = "sha256:37e50501baaa935f13a1820ab2114f74313b5cb4cfff8146acb8c5b18cdced2a"}, + {file = "zstandard-0.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:b4e671c4c0804cdf752be26f260058bb858fbdaaef1340af170635913ecca01e"}, + {file = "zstandard-0.17.0.tar.gz", hash = "sha256:fa9194cb91441df7242aa3ddc4cb184be38876cb10dd973674887f334bafbfb6"}, ] diff --git a/pyproject.toml b/pyproject.toml index 734bcac..d2a90a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,16 +25,16 @@ Rx = "^3.2.0" rxpy-backpressure = "^1.0.0" solana = "^0.21.0" websocket-client = "^1.2.1" -zstandard = "^0.16.0" +zstandard = "^0.17.0" [tool.poetry.dev-dependencies] -autopep8 = "^1.5.7" +black = "^22.1.0" flake8 = "^4.0.1" ipykernel = "^6.7.0" Jinja2 = "^3.0.3" mypy = "^0.931" pandas-stubs = "^1.2.0.43" -pytest = "^6.2.5" +pytest = "^7.0.0" types-cachetools = "^4.2.9" types-certifi = "^2021.10.8.1" types-requests = "^2.27.7" diff --git a/scripts/run-jupyter b/scripts/run-jupyter deleted file mode 100755 index ca6aede..0000000 --- a/scripts/run-jupyter +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - -CURRENT_DIRECTORY="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -START_COMMAND="start.sh jupyter lab --NotebookApp.iopub_data_rate_limit=9e9" -if [ "$1" == "notebook" ]; then - START_COMMAND="start-notebook.sh" -fi - -TAG=${1:-latest} - -docker run -it --rm \ - -e GRANT_SUDO=yes --user root \ - -p 8888:8888 \ - --name mango-explorer \ - -v ${CURRENT_DIRECTORY}/..:/home/jovyan/work \ - opinionatedgeek/mango-explorer:${TAG} \ - $START_COMMAND diff --git a/tests/calculations/test_healthcalculator.py b/tests/calculations/test_healthcalculator.py index d5aa5dd..f61bf4d 100644 --- a/tests/calculations/test_healthcalculator.py +++ b/tests/calculations/test_healthcalculator.py @@ -7,7 +7,9 @@ from mango.calculators.healthcalculator import HealthType, HealthCalculator def test_empty() -> None: context = fake_context() - group, cache, account, open_orders = load_data_from_directory("tests/testdata/empty") + group, cache, account, open_orders = load_data_from_directory( + "tests/testdata/empty" + ) actual = HealthCalculator(context, HealthType.INITIAL) health = actual.calculate(account, open_orders, group, cache) @@ -17,7 +19,9 @@ def test_empty() -> None: def test_1deposit() -> None: context = fake_context() - group, cache, account, open_orders = load_data_from_directory("tests/testdata/1deposit") + group, cache, account, open_orders = load_data_from_directory( + "tests/testdata/1deposit" + ) actual = HealthCalculator(context, HealthType.INITIAL) health = actual.calculate(account, open_orders, group, cache) @@ -27,7 +31,9 @@ def test_1deposit() -> None: def test_account1() -> None: context = fake_context() - group, cache, account, open_orders = load_data_from_directory("tests/testdata/account1") + group, cache, account, open_orders = load_data_from_directory( + "tests/testdata/account1" + ) actual = HealthCalculator(context, HealthType.INITIAL) health = actual.calculate(account, open_orders, group, cache) @@ -38,7 +44,9 @@ def test_account1() -> None: def test_account2() -> None: context = fake_context() - group, cache, account, open_orders = load_data_from_directory("tests/testdata/account2") + group, cache, account, open_orders = load_data_from_directory( + "tests/testdata/account2" + ) actual = HealthCalculator(context, HealthType.INITIAL) health = actual.calculate(account, open_orders, group, cache) @@ -49,7 +57,9 @@ def test_account2() -> None: def test_account3() -> None: context = fake_context() - group, cache, account, open_orders = load_data_from_directory("tests/testdata/account3") + group, cache, account, open_orders = load_data_from_directory( + "tests/testdata/account3" + ) actual = HealthCalculator(context, HealthType.INITIAL) health = actual.calculate(account, open_orders, group, cache) @@ -61,7 +71,9 @@ def test_account3() -> None: def test_account4() -> None: context = fake_context() - group, cache, account, open_orders = load_data_from_directory("tests/testdata/account4") + group, cache, account, open_orders = load_data_from_directory( + "tests/testdata/account4" + ) actual = HealthCalculator(context, HealthType.INITIAL) health = actual.calculate(account, open_orders, group, cache) diff --git a/tests/context.py b/tests/context.py index a12963e..59488dc 100644 --- a/tests/context.py +++ b/tests/context.py @@ -4,8 +4,8 @@ import mango as mango import os 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__), ".."))) @contextmanager diff --git a/tests/data.py b/tests/data.py index f277d79..3a7a172 100644 --- a/tests/data.py +++ b/tests/data.py @@ -7,19 +7,35 @@ from decimal import Decimal def load_group(filename: str) -> mango.Group: account_info: mango.AccountInfo = mango.AccountInfo.load_json(filename) - mainnet_token_lookup: mango.InstrumentLookup = mango.IdsJsonTokenLookup("mainnet", "mainnet.1") - devnet_token_lookup: mango.InstrumentLookup = mango.IdsJsonTokenLookup("devnet", "devnet.2") - devnet_non_spl_instrument_lookup: mango.InstrumentLookup = mango.NonSPLInstrumentLookup.load( - mango.NonSPLInstrumentLookup.DefaultDevnetDataFilepath) + mainnet_token_lookup: mango.InstrumentLookup = mango.IdsJsonTokenLookup( + "mainnet", "mainnet.1" + ) + devnet_token_lookup: mango.InstrumentLookup = mango.IdsJsonTokenLookup( + "devnet", "devnet.2" + ) + devnet_non_spl_instrument_lookup: mango.InstrumentLookup = ( + mango.NonSPLInstrumentLookup.load( + mango.NonSPLInstrumentLookup.DefaultDevnetDataFilepath + ) + ) instrument_lookup: mango.InstrumentLookup = mango.CompoundInstrumentLookup( - [mainnet_token_lookup, devnet_token_lookup, devnet_non_spl_instrument_lookup]) - mainnet_market_lookup: mango.MarketLookup = mango.IdsJsonMarketLookup("mainnet", instrument_lookup) - devnet_market_lookup: mango.MarketLookup = mango.IdsJsonMarketLookup("devnet", instrument_lookup) - market_lookup: mango.MarketLookup = mango.CompoundMarketLookup([mainnet_market_lookup, devnet_market_lookup]) + [mainnet_token_lookup, devnet_token_lookup, devnet_non_spl_instrument_lookup] + ) + mainnet_market_lookup: mango.MarketLookup = mango.IdsJsonMarketLookup( + "mainnet", instrument_lookup + ) + devnet_market_lookup: mango.MarketLookup = mango.IdsJsonMarketLookup( + "devnet", instrument_lookup + ) + market_lookup: mango.MarketLookup = mango.CompoundMarketLookup( + [mainnet_market_lookup, devnet_market_lookup] + ) return mango.Group.parse(account_info, "devnet.2", instrument_lookup, market_lookup) -def load_account(filename: str, group: mango.Group, cache: mango.Cache) -> mango.Account: +def load_account( + filename: str, group: mango.Group, cache: mango.Cache +) -> mango.Account: account_info: mango.AccountInfo = mango.AccountInfo.load_json(filename) return mango.Account.parse(account_info, group, cache) @@ -45,7 +61,11 @@ def load_node_bank(filename: str) -> mango.NodeBank: return mango.NodeBank.parse(account_info) -def load_data_from_directory(directory_path: str) -> typing.Tuple[mango.Group, mango.Cache, mango.Account, typing.Dict[str, mango.OpenOrders]]: +def load_data_from_directory( + directory_path: str, +) -> typing.Tuple[ + mango.Group, mango.Cache, mango.Account, typing.Dict[str, mango.OpenOrders] +]: all_openorders = {} for filepath in glob.iglob(f"{directory_path}/openorders*.json"): openorders = load_openorders(filepath) diff --git a/tests/fakes.py b/tests/fakes.py index 7858f13..4829aa8 100644 --- a/tests/fakes.py +++ b/tests/fakes.py @@ -20,20 +20,39 @@ class MockCompatibleClient(Client): super().__init__("http://localhost", Commitment("processed")) self.token_accounts_by_owner: typing.Sequence[typing.Any] = [] - def get_token_accounts_by_owner(self, *args: typing.Any, **kwargs: typing.Any) -> RPCResponse: + def get_token_accounts_by_owner( + self, *args: typing.Any, **kwargs: typing.Any + ) -> RPCResponse: return RPCResponse(result={"value": self.token_accounts_by_owner}) - def get_minimum_balance_for_rent_exemption(size, *args: typing.Any, **kwargs: typing.Any) -> RPCResponse: + def get_minimum_balance_for_rent_exemption( + size, *args: typing.Any, **kwargs: typing.Any + ) -> RPCResponse: return RPCResponse(result=27) class MockClient(mango.BetterClient): def __init__(self) -> None: - rpc = mango.RPCCaller("fake", "http://localhost", "ws://localhost", -1, - [], mango.SlotHolder(), mango.InstructionReporter()) + rpc = mango.RPCCaller( + "fake", + "http://localhost", + "ws://localhost", + -1, + [], + mango.SlotHolder(), + mango.InstructionReporter(), + ) compound = mango.CompoundRPCCaller("fake", [rpc]) - super().__init__(MockCompatibleClient(), "test", "local", Commitment("processed"), - False, "base64", 0, compound) + super().__init__( + MockCompatibleClient(), + "test", + "local", + Commitment("processed"), + False, + "base64", + 0, + compound, + ) def fake_public_key() -> PublicKey: @@ -41,37 +60,52 @@ def fake_public_key() -> PublicKey: def fake_seeded_public_key(seed: str) -> PublicKey: - return PublicKey.create_with_seed(PublicKey("11111111111111111111111111111112"), seed, PublicKey("11111111111111111111111111111111")) + return PublicKey.create_with_seed( + PublicKey("11111111111111111111111111111112"), + seed, + PublicKey("11111111111111111111111111111111"), + ) -def fake_context(mango_program_address: typing.Optional[PublicKey] = None) -> mango.Context: - context = mango.Context(name="Mango Test", - cluster_name="test", - cluster_urls=[ - mango.ClusterUrlData(rpc="http://localhost"), - mango.ClusterUrlData(rpc="http://localhost") - ], - skip_preflight=False, - commitment="processed", - encoding="base64", - blockhash_cache_duration=0, - http_request_timeout=-1, - stale_data_pauses_before_retry=[], - mango_program_address=mango_program_address or fake_seeded_public_key( - "Mango program address"), - serum_program_address=fake_seeded_public_key("Serum program address"), - group_name="TEST_GROUP", - group_address=fake_seeded_public_key("group ID"), - gma_chunk_size=Decimal(20), - gma_chunk_pause=Decimal(25), - reflink=None, - instrument_lookup=mango.IdsJsonTokenLookup("devnet", "devnet.2"), - market_lookup=mango.NullMarketLookup()) +def fake_context( + mango_program_address: typing.Optional[PublicKey] = None, +) -> mango.Context: + context = mango.Context( + name="Mango Test", + cluster_name="test", + cluster_urls=[ + mango.ClusterUrlData(rpc="http://localhost"), + mango.ClusterUrlData(rpc="http://localhost"), + ], + skip_preflight=False, + commitment="processed", + encoding="base64", + blockhash_cache_duration=0, + http_request_timeout=-1, + stale_data_pauses_before_retry=[], + mango_program_address=mango_program_address + or fake_seeded_public_key("Mango program address"), + serum_program_address=fake_seeded_public_key("Serum program address"), + group_name="TEST_GROUP", + group_address=fake_seeded_public_key("group ID"), + gma_chunk_size=Decimal(20), + gma_chunk_pause=Decimal(25), + reflink=None, + instrument_lookup=mango.IdsJsonTokenLookup("devnet", "devnet.2"), + market_lookup=mango.NullMarketLookup(), + ) context.client = MockClient() return context -def fake_account_info(address: typing.Optional[PublicKey] = None, executable: bool = False, lamports: Decimal = Decimal(0), owner: PublicKey = fake_public_key(), rent_epoch: Decimal = Decimal(0), data: bytes = bytes([0])) -> mango.AccountInfo: +def fake_account_info( + address: typing.Optional[PublicKey] = None, + executable: bool = False, + lamports: Decimal = Decimal(0), + owner: PublicKey = fake_public_key(), + rent_epoch: Decimal = Decimal(0), + data: bytes = bytes([0]), +) -> mango.AccountInfo: if address is None: address = fake_public_key() return mango.AccountInfo(address, executable, lamports, owner, rent_epoch, data) @@ -82,13 +116,30 @@ def fake_instrument(symbol: str = "FAKE", decimals: int = 6) -> mango.Instrument def fake_token(symbol: str = "FAKE", decimals: int = 6) -> mango.Token: - return mango.Token(symbol, f"Fake Token ({symbol})", Decimal(decimals), fake_seeded_public_key(f"fake token ({symbol})")) + return mango.Token( + symbol, + f"Fake Token ({symbol})", + Decimal(decimals), + fake_seeded_public_key(f"fake token ({symbol})"), + ) def fake_perp_account() -> mango.PerpAccount: - return mango.PerpAccount(Decimal(0), Decimal(0), Decimal(0), Decimal(0), Decimal(0), - Decimal(0), Decimal(0), Decimal(0), fake_instrument_value(), mango.PerpOpenOrders([]), - mango.NullLotSizeConverter(), fake_instrument_value(), Decimal(0)) + return mango.PerpAccount( + Decimal(0), + Decimal(0), + Decimal(0), + Decimal(0), + Decimal(0), + Decimal(0), + Decimal(0), + Decimal(0), + fake_instrument_value(), + mango.PerpOpenOrders([]), + mango.NullLotSizeConverter(), + fake_instrument_value(), + Decimal(0), + ) def fake_token_bank(symbol: str = "FAKE") -> mango.TokenBank: @@ -97,7 +148,12 @@ def fake_token_bank(symbol: str = "FAKE") -> mango.TokenBank: def fake_market() -> PySerumMarket: # Container = NamedTuple("Container", [("own_address", PublicKey), ("vault_signer_nonce", int)]) - container = construct.Container({"own_address": fake_seeded_public_key("market address"), "vault_signer_nonce": 2}) + container = construct.Container( + { + "own_address": fake_seeded_public_key("market address"), + "vault_signer_nonce": 2, + } + ) # container: Container[typing.Any] = Container( # own_address=fake_seeded_public_key("market address"), vault_signer_nonce=2) state = PySerumMarketState(container, fake_seeded_public_key("program ID"), 6, 6) @@ -113,20 +169,40 @@ def fake_market() -> PySerumMarket: def fake_spot_market_stub() -> mango.SpotMarketStub: - return mango.SpotMarketStub(fake_seeded_public_key("program ID"), fake_seeded_public_key("spot market"), fake_token("BASE"), fake_token("QUOTE"), fake_seeded_public_key("group address")) + return mango.SpotMarketStub( + fake_seeded_public_key("program ID"), + fake_seeded_public_key("spot market"), + fake_token("BASE"), + fake_token("QUOTE"), + fake_seeded_public_key("group address"), + ) -def fake_loaded_market(base_lot_size: Decimal = Decimal(1), quote_lot_size: Decimal = Decimal(1)) -> mango.LoadedMarket: +def fake_loaded_market( + base_lot_size: Decimal = Decimal(1), quote_lot_size: Decimal = Decimal(1) +) -> mango.LoadedMarket: base = fake_token("BASE") quote = fake_token("QUOTE") - return mango.LoadedMarket(fake_seeded_public_key("program ID"), fake_seeded_public_key("perp market"), mango.InventorySource.ACCOUNT, base, quote, mango.LotSizeConverter(base, base_lot_size, quote, quote_lot_size)) + return mango.LoadedMarket( + fake_seeded_public_key("program ID"), + fake_seeded_public_key("perp market"), + mango.InventorySource.ACCOUNT, + base, + quote, + mango.LotSizeConverter(base, base_lot_size, quote, quote_lot_size), + ) def fake_token_account() -> mango.TokenAccount: token_account_info = fake_account_info() token = fake_token() token_value = mango.InstrumentValue(token, Decimal("100")) - return mango.TokenAccount(token_account_info, mango.Version.V1, fake_seeded_public_key("owner"), token_value) + return mango.TokenAccount( + token_account_info, + mango.Version.V1, + fake_seeded_public_key("owner"), + token_value, + ) def fake_instrument_value(value: Decimal = Decimal(100)) -> mango.InstrumentValue: @@ -139,30 +215,65 @@ def fake_wallet() -> mango.Wallet: return wallet -def fake_order(price: Decimal = Decimal(1), quantity: Decimal = Decimal(1), side: mango.Side = mango.Side.BUY, order_type: mango.OrderType = mango.OrderType.LIMIT) -> mango.Order: - return mango.Order.from_basic_info(side=side, price=price, quantity=quantity, order_type=order_type) +def fake_order( + price: Decimal = Decimal(1), + quantity: Decimal = Decimal(1), + side: mango.Side = mango.Side.BUY, + order_type: mango.OrderType = mango.OrderType.LIMIT, +) -> mango.Order: + return mango.Order.from_basic_info( + side=side, price=price, quantity=quantity, order_type=order_type + ) # serum ID structure - 16-byte 'int': low 8 bytes is a sequence number, high 8 bytes is price def fake_order_id(index: int, price: int) -> int: # price needs to be max of 64bit/8bytes, considering signed int is not permitted - if index > (2 ** 64) - 1 or price > (2 ** 64) - 1: - raise ValueError(f"Provided index '{index}' or price '{price}' is bigger than 8 bytes int") - index_bytes = index.to_bytes(8, byteorder='big', signed=False) - price_bytes = price.to_bytes(8, byteorder='big', signed=False) - return int.from_bytes((price_bytes + index_bytes), byteorder='big', signed=False) + if index > (2**64) - 1 or price > (2**64) - 1: + raise ValueError( + f"Provided index '{index}' or price '{price}' is bigger than 8 bytes int" + ) + index_bytes = index.to_bytes(8, byteorder="big", signed=False) + price_bytes = price.to_bytes(8, byteorder="big", signed=False) + return int.from_bytes((price_bytes + index_bytes), byteorder="big", signed=False) -def fake_price(market: mango.Market = fake_loaded_market(), price: Decimal = Decimal(100), bid: Decimal = Decimal(99), ask: Decimal = Decimal(101)) -> mango.Price: - return mango.Price(mango.OracleSource("test", "test", mango.SupportedOracleFeature.TOP_BID_AND_OFFER, market), datetime.datetime.now(), market, bid, price, ask, Decimal(0)) +def fake_price( + market: mango.Market = fake_loaded_market(), + price: Decimal = Decimal(100), + bid: Decimal = Decimal(99), + ask: Decimal = Decimal(101), +) -> mango.Price: + return mango.Price( + mango.OracleSource( + "test", "test", mango.SupportedOracleFeature.TOP_BID_AND_OFFER, market + ), + datetime.datetime.now(), + market, + bid, + price, + ask, + Decimal(0), + ) def fake_placed_orders_container() -> mango.PlacedOrdersContainer: return mango.PerpOpenOrders([]) -def fake_inventory(incentives: Decimal = Decimal(1), available: Decimal = Decimal(100), base: Decimal = Decimal(10), quote: Decimal = Decimal(10)) -> mango.Inventory: - return mango.Inventory(mango.InventorySource.SPL_TOKENS, fake_instrument_value(incentives), fake_instrument_value(available), fake_instrument_value(base), fake_instrument_value(quote)) +def fake_inventory( + incentives: Decimal = Decimal(1), + available: Decimal = Decimal(100), + base: Decimal = Decimal(10), + quote: Decimal = Decimal(10), +) -> mango.Inventory: + return mango.Inventory( + mango.InventorySource.SPL_TOKENS, + fake_instrument_value(incentives), + fake_instrument_value(available), + fake_instrument_value(base), + fake_instrument_value(quote), + ) def fake_bids() -> typing.Sequence[mango.Order]: @@ -174,24 +285,58 @@ def fake_asks() -> typing.Sequence[mango.Order]: def fake_account_slot() -> mango.AccountSlot: - return mango.AccountSlot(1, fake_instrument(), fake_token_bank(), fake_token_bank(), Decimal(1), - fake_instrument_value(), Decimal(0), fake_instrument_value(), - fake_seeded_public_key("open_orders"), None) + return mango.AccountSlot( + 1, + fake_instrument(), + fake_token_bank(), + fake_token_bank(), + Decimal(1), + fake_instrument_value(), + Decimal(0), + fake_instrument_value(), + fake_seeded_public_key("open_orders"), + None, + ) def fake_account(address: typing.Optional[PublicKey] = None) -> mango.Account: meta_data = mango.Metadata(mango.layouts.DATA_TYPE.Account, mango.Version.V1, True) quote = fake_account_slot() - return mango.Account(fake_account_info(address=address), mango.Version.V1, meta_data, "GROUPNAME", - fake_seeded_public_key("group"), fake_seeded_public_key("owner"), "INFO", - quote, [], [], [], Decimal(1), False, False, fake_seeded_public_key("advanced_orders"), - False, fake_seeded_public_key("delegate")) + return mango.Account( + fake_account_info(address=address), + mango.Version.V1, + meta_data, + "GROUPNAME", + fake_seeded_public_key("group"), + fake_seeded_public_key("owner"), + "INFO", + quote, + [], + [], + [], + Decimal(1), + False, + False, + fake_seeded_public_key("advanced_orders"), + False, + fake_seeded_public_key("delegate"), + ) def fake_root_bank() -> mango.RootBank: meta_data = mango.Metadata(mango.layouts.DATA_TYPE.RootBank, mango.Version.V1, True) - return mango.RootBank(fake_account_info(), mango.Version.V1, meta_data, Decimal(0), Decimal(0), - Decimal(0), [], Decimal(0), Decimal(0), datetime.datetime.now()) + return mango.RootBank( + fake_account_info(), + mango.Version.V1, + meta_data, + Decimal(0), + Decimal(0), + Decimal(0), + [], + Decimal(0), + Decimal(0), + datetime.datetime.now(), + ) def fake_cache() -> mango.Cache: @@ -226,11 +371,30 @@ def fake_group(address: typing.Optional[PublicKey] = None) -> mango.Group: referral_share_centibps = Decimal(8) referral_mngo_required = Decimal(9) - return mango.Group(account_info, mango.Version.V1, name, meta_data, quote_info, [], [], - signer_nonce, signer_key, admin_key, serum_program_address, cache_key, - 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 mango.Group( + account_info, + mango.Version.V1, + name, + meta_data, + quote_info, + [], + [], + signer_nonce, + signer_key, + admin_key, + serum_program_address, + cache_key, + 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, + ) def fake_prices(prices: typing.Sequence[str]) -> typing.Sequence[mango.InstrumentValue]: @@ -251,29 +415,48 @@ def fake_prices(prices: typing.Sequence[str]) -> typing.Sequence[mango.Instrumen ] -def fake_open_orders(base_token_free: Decimal = Decimal(0), base_token_total: Decimal = Decimal(0), - quote_token_free: Decimal = Decimal(0), quote_token_total: Decimal = Decimal(0), - referrer_rebate_accrued: Decimal = Decimal(0)) -> mango.OpenOrders: +def fake_open_orders( + base_token_free: Decimal = Decimal(0), + base_token_total: Decimal = Decimal(0), + quote_token_free: Decimal = Decimal(0), + quote_token_total: Decimal = Decimal(0), + referrer_rebate_accrued: Decimal = Decimal(0), +) -> mango.OpenOrders: account_info = fake_account_info() program_address = fake_seeded_public_key("program address") market = fake_seeded_public_key("market") owner = fake_seeded_public_key("owner") - flags = mango.AccountFlags(mango.Version.V1, True, False, True, False, False, False, False, False) - return mango.OpenOrders(account_info, mango.Version.V1, program_address, flags, market, - owner, base_token_free, base_token_total, quote_token_free, - quote_token_total, [], referrer_rebate_accrued) + flags = mango.AccountFlags( + mango.Version.V1, True, False, True, False, False, False, False, False + ) + return mango.OpenOrders( + account_info, + mango.Version.V1, + program_address, + flags, + market, + owner, + base_token_free, + base_token_total, + quote_token_free, + quote_token_total, + [], + referrer_rebate_accrued, + ) -def fake_model_state(order_owner: typing.Optional[PublicKey] = None, - market: typing.Optional[mango.Market] = None, - group: typing.Optional[mango.Group] = None, - account: typing.Optional[mango.Account] = None, - price: typing.Optional[mango.Price] = None, - placed_orders_container: typing.Optional[mango.PlacedOrdersContainer] = None, - inventory: typing.Optional[mango.Inventory] = None, - orderbook: typing.Optional[mango.OrderBook] = None, - event_queue: typing.Optional[mango.EventQueue] = None) -> mango.ModelState: +def fake_model_state( + order_owner: typing.Optional[PublicKey] = None, + market: typing.Optional[mango.Market] = None, + group: typing.Optional[mango.Group] = None, + account: typing.Optional[mango.Account] = None, + price: typing.Optional[mango.Price] = None, + placed_orders_container: typing.Optional[mango.PlacedOrdersContainer] = None, + inventory: typing.Optional[mango.Inventory] = None, + orderbook: typing.Optional[mango.OrderBook] = None, + event_queue: typing.Optional[mango.EventQueue] = None, +) -> mango.ModelState: order_owner = order_owner or fake_seeded_public_key("order owner") market = market or fake_loaded_market() group = group or fake_group() @@ -281,21 +464,46 @@ def fake_model_state(order_owner: typing.Optional[PublicKey] = None, price = price or fake_price() placed_orders_container = placed_orders_container or fake_placed_orders_container() inventory = inventory or fake_inventory() - orderbook = orderbook or mango.OrderBook("FAKE", NullLotSizeConverter(), fake_bids(), fake_asks()) + orderbook = orderbook or mango.OrderBook( + "FAKE", NullLotSizeConverter(), fake_bids(), fake_asks() + ) event_queue = event_queue or mango.NullEventQueue() - group_watcher: mango.ManualUpdateWatcher[mango.Group] = mango.ManualUpdateWatcher(group) - account_watcher: mango.ManualUpdateWatcher[mango.Account] = mango.ManualUpdateWatcher(account) - price_watcher: mango.ManualUpdateWatcher[mango.Price] = mango.ManualUpdateWatcher(price) + group_watcher: mango.ManualUpdateWatcher[mango.Group] = mango.ManualUpdateWatcher( + group + ) + account_watcher: mango.ManualUpdateWatcher[ + mango.Account + ] = mango.ManualUpdateWatcher(account) + price_watcher: mango.ManualUpdateWatcher[mango.Price] = mango.ManualUpdateWatcher( + price + ) placed_orders_container_watcher: mango.ManualUpdateWatcher[ - mango.PlacedOrdersContainer] = mango.ManualUpdateWatcher(placed_orders_container) - inventory_watcher: mango.ManualUpdateWatcher[mango.Inventory] = mango.ManualUpdateWatcher(inventory) - orderbook_watcher: mango.ManualUpdateWatcher[mango.OrderBook] = mango.ManualUpdateWatcher(orderbook) - event_queue_watcher: mango.ManualUpdateWatcher[mango.EventQueue] = mango.ManualUpdateWatcher(event_queue) + mango.PlacedOrdersContainer + ] = mango.ManualUpdateWatcher(placed_orders_container) + inventory_watcher: mango.ManualUpdateWatcher[ + mango.Inventory + ] = mango.ManualUpdateWatcher(inventory) + orderbook_watcher: mango.ManualUpdateWatcher[ + mango.OrderBook + ] = mango.ManualUpdateWatcher(orderbook) + event_queue_watcher: mango.ManualUpdateWatcher[ + mango.EventQueue + ] = mango.ManualUpdateWatcher(event_queue) - return mango.ModelState(order_owner, market, group_watcher, account_watcher, price_watcher, - placed_orders_container_watcher, inventory_watcher, orderbook_watcher, - event_queue_watcher) + return mango.ModelState( + order_owner, + market, + group_watcher, + account_watcher, + price_watcher, + placed_orders_container_watcher, + inventory_watcher, + orderbook_watcher, + event_queue_watcher, + ) def fake_mango_instruction() -> mango.MangoInstruction: - return mango.MangoInstruction(mango.InstructionType.PlacePerpOrder, "", [fake_seeded_public_key("account")]) + return mango.MangoInstruction( + mango.InstructionType.PlacePerpOrder, "", [fake_seeded_public_key("account")] + ) diff --git a/tests/layouts/test_layouts.py b/tests/layouts/test_layouts.py index 1dccb5e..2008022 100644 --- a/tests/layouts/test_layouts.py +++ b/tests/layouts/test_layouts.py @@ -14,19 +14,33 @@ def test_group_layout() -> None: # Not an exhaustive check, just a few key areas assert group.num_oracles == 10 assert len(group.tokens) == 16 - assert group.tokens[0].mint == PublicKey("Bb9bsTQa1bGEtQ5KagGkvSHyuLqDWumFUcRqFusFNJWC") - assert group.tokens[1].mint == PublicKey("3UNBZ6o52WTWwjac2kPUb4FyodhU1vFkRJheu1Sh2TvU") + assert group.tokens[0].mint == PublicKey( + "Bb9bsTQa1bGEtQ5KagGkvSHyuLqDWumFUcRqFusFNJWC" + ) + assert group.tokens[1].mint == PublicKey( + "3UNBZ6o52WTWwjac2kPUb4FyodhU1vFkRJheu1Sh2TvU" + ) assert group.tokens[14].mint is None - assert group.tokens[15].mint == PublicKey("8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN") + assert group.tokens[15].mint == PublicKey( + "8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN" + ) assert len(group.spot_markets) == 15 - assert group.spot_markets[0].spot_market == PublicKey("BqAmk715myHomPTMtuSydqqntQ9PDnJ1WoXMc2oAUomj") - assert group.spot_markets[9].spot_market == PublicKey("Cyc11qk1FQTmQNFHMHEbwLsgdnHzFK2DrowiKwrGaHxC") + assert group.spot_markets[0].spot_market == PublicKey( + "BqAmk715myHomPTMtuSydqqntQ9PDnJ1WoXMc2oAUomj" + ) + assert group.spot_markets[9].spot_market == PublicKey( + "Cyc11qk1FQTmQNFHMHEbwLsgdnHzFK2DrowiKwrGaHxC" + ) assert group.spot_markets[14].spot_market is None assert len(group.perp_markets) == 15 - assert group.perp_markets[0].perp_market == PublicKey("Gnd9WTaFjJwZU8XEpoB8EYfx5GryJ1dRDw9EzwLtX2b") - assert group.perp_markets[9].perp_market == PublicKey("6JZZbRjmqCke4Nm9XkzsrXFzM7LdmFHndDA5gcdiRvEx") + assert group.perp_markets[0].perp_market == PublicKey( + "Gnd9WTaFjJwZU8XEpoB8EYfx5GryJ1dRDw9EzwLtX2b" + ) + assert group.perp_markets[9].perp_market == PublicKey( + "6JZZbRjmqCke4Nm9XkzsrXFzM7LdmFHndDA5gcdiRvEx" + ) assert group.perp_markets[14].perp_market is None assert len(group.oracles) == 15 @@ -37,10 +51,14 @@ def test_group_layout() -> None: assert group.signer_nonce == 0 assert group.signer_key == PublicKey("9GXvznfEep9yEsvH4CQzqNy5GH81FNk1HDeAR8UjefSf") assert group.admin == PublicKey("Cwg1f6m4m3DGwMEbmsbAfDtUToUf5jRdKrJSGD7GfZCB") - assert group.serum_program_address == PublicKey("DESVgJVGajEgKGXhb6XmqDHGz3VjdgP7rEVESBgxmroY") + assert group.serum_program_address == PublicKey( + "DESVgJVGajEgKGXhb6XmqDHGz3VjdgP7rEVESBgxmroY" + ) assert group.cache == PublicKey("PJhM2enPpZH7E9wgw7Sqt8S2p4mr3Bc7SycawQwfY7b") assert group.valid_interval == 5 - assert group.insurance_vault == PublicKey("14gfuPWjUQnYXpsxs4WgsjafUrJctKkR9AMFH7fjvTgR") + assert group.insurance_vault == PublicKey( + "14gfuPWjUQnYXpsxs4WgsjafUrJctKkR9AMFH7fjvTgR" + ) assert group.srm_vault == PublicKey("23Z3FWjXdt18FiZUwfsnQkUDvF14MneS7uoMYytfNe3G") assert group.msrm_vault == PublicKey("Qjf6vWMKPwEMLMM6ci9tudUWZ2t8zVXHqHverDWFx9S") assert group.fees_vault is None diff --git a/tests/marketmaking/orderchain/test_afteraccumulateddepthelement.py b/tests/marketmaking/orderchain/test_afteraccumulateddepthelement.py index 0676d1a..85b7121 100644 --- a/tests/marketmaking/orderchain/test_afteraccumulateddepthelement.py +++ b/tests/marketmaking/orderchain/test_afteraccumulateddepthelement.py @@ -7,7 +7,9 @@ from ...fakes import fake_context, fake_model_state, fake_order, fake_seeded_pub from decimal import Decimal from solana.publickey import PublicKey -from mango.marketmaking.orderchain.afteraccumulateddepthelement import AfterAccumulatedDepthElement +from mango.marketmaking.orderchain.afteraccumulateddepthelement import ( + AfterAccumulatedDepthElement, +) bids: typing.Sequence[mango.Order] = [ fake_order(price=Decimal(78), quantity=Decimal(1), side=mango.Side.BUY), @@ -15,7 +17,7 @@ bids: typing.Sequence[mango.Order] = [ fake_order(price=Decimal(76), quantity=Decimal(1), side=mango.Side.BUY), fake_order(price=Decimal(75), quantity=Decimal(5), side=mango.Side.BUY), fake_order(price=Decimal(74), quantity=Decimal(3), side=mango.Side.BUY), - fake_order(price=Decimal(73), quantity=Decimal(7), side=mango.Side.BUY) + fake_order(price=Decimal(73), quantity=Decimal(7), side=mango.Side.BUY), ] asks: typing.Sequence[mango.Order] = [ fake_order(price=Decimal(82), quantity=Decimal(3), side=mango.Side.SELL), @@ -23,22 +25,29 @@ asks: typing.Sequence[mango.Order] = [ fake_order(price=Decimal(84), quantity=Decimal(1), side=mango.Side.SELL), fake_order(price=Decimal(85), quantity=Decimal(3), side=mango.Side.SELL), fake_order(price=Decimal(86), quantity=Decimal(3), side=mango.Side.SELL), - fake_order(price=Decimal(87), quantity=Decimal(7), side=mango.Side.SELL) + fake_order(price=Decimal(87), quantity=Decimal(7), side=mango.Side.SELL), ] -orderbook: mango.OrderBook = mango.OrderBook("TEST", mango.NullLotSizeConverter(), bids, asks) +orderbook: mango.OrderBook = mango.OrderBook( + "TEST", mango.NullLotSizeConverter(), bids, asks +) model_state = fake_model_state(orderbook=orderbook) def test_from_args() -> None: args: argparse.Namespace = argparse.Namespace( - afteraccumulateddepth_depth=None, afteraccumulateddepth_adjustment_ticks=None) - actual: AfterAccumulatedDepthElement = AfterAccumulatedDepthElement.from_command_line_parameters(args) + afteraccumulateddepth_depth=None, afteraccumulateddepth_adjustment_ticks=None + ) + actual: AfterAccumulatedDepthElement = ( + AfterAccumulatedDepthElement.from_command_line_parameters(args) + ) assert actual is not None def test_bid_price_updated() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(78), quantity=Decimal(7), side=mango.Side.BUY) + order: mango.Order = fake_order( + price=Decimal(78), quantity=Decimal(7), side=mango.Side.BUY + ) actual: AfterAccumulatedDepthElement = AfterAccumulatedDepthElement(None) result = actual.process(context, model_state, [order]) @@ -48,7 +57,9 @@ def test_bid_price_updated() -> None: def test_ask_price_updated() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(82), quantity=Decimal(6), side=mango.Side.SELL) + order: mango.Order = fake_order( + price=Decimal(82), quantity=Decimal(6), side=mango.Side.SELL + ) actual: AfterAccumulatedDepthElement = AfterAccumulatedDepthElement(None) result = actual.process(context, model_state, [order]) @@ -62,23 +73,33 @@ def test_accumulation_ignores_own_orders_updated() -> None: fake_order(price=Decimal(78), quantity=Decimal(1), side=mango.Side.BUY), fake_order(price=Decimal(77), quantity=Decimal(2), side=mango.Side.BUY), fake_order(price=Decimal(76), quantity=Decimal(1), side=mango.Side.BUY), - fake_order(price=Decimal(75), quantity=Decimal(5), side=mango.Side.BUY).with_owner(order_owner), + fake_order( + price=Decimal(75), quantity=Decimal(5), side=mango.Side.BUY + ).with_owner(order_owner), fake_order(price=Decimal(74), quantity=Decimal(3), side=mango.Side.BUY), - fake_order(price=Decimal(73), quantity=Decimal(7), side=mango.Side.BUY) + fake_order(price=Decimal(73), quantity=Decimal(7), side=mango.Side.BUY), ] asks: typing.Sequence[mango.Order] = [ fake_order(price=Decimal(82), quantity=Decimal(3), side=mango.Side.SELL), fake_order(price=Decimal(83), quantity=Decimal(1), side=mango.Side.SELL), fake_order(price=Decimal(84), quantity=Decimal(1), side=mango.Side.SELL), - fake_order(price=Decimal(85), quantity=Decimal(3), side=mango.Side.SELL).with_owner(order_owner), + fake_order( + price=Decimal(85), quantity=Decimal(3), side=mango.Side.SELL + ).with_owner(order_owner), fake_order(price=Decimal(86), quantity=Decimal(3), side=mango.Side.SELL), - fake_order(price=Decimal(87), quantity=Decimal(7), side=mango.Side.SELL) + fake_order(price=Decimal(87), quantity=Decimal(7), side=mango.Side.SELL), ] - orderbook: mango.OrderBook = mango.OrderBook("TEST", mango.NullLotSizeConverter(), bids, asks) + orderbook: mango.OrderBook = mango.OrderBook( + "TEST", mango.NullLotSizeConverter(), bids, asks + ) model_state = fake_model_state(order_owner=order_owner, orderbook=orderbook) context = fake_context() - buy: mango.Order = fake_order(price=Decimal(78), quantity=Decimal(6), side=mango.Side.BUY) - sell: mango.Order = fake_order(price=Decimal(82), quantity=Decimal(6), side=mango.Side.SELL) + buy: mango.Order = fake_order( + price=Decimal(78), quantity=Decimal(6), side=mango.Side.BUY + ) + sell: mango.Order = fake_order( + price=Decimal(82), quantity=Decimal(6), side=mango.Side.SELL + ) actual: AfterAccumulatedDepthElement = AfterAccumulatedDepthElement(None) result = actual.process(context, model_state, [buy, sell]) @@ -89,9 +110,13 @@ def test_accumulation_ignores_own_orders_updated() -> None: def test_bid_price_updated_at_instead_of_after() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(78), quantity=Decimal(7), side=mango.Side.BUY) + order: mango.Order = fake_order( + price=Decimal(78), quantity=Decimal(7), side=mango.Side.BUY + ) - actual: AfterAccumulatedDepthElement = AfterAccumulatedDepthElement(None, Decimal(0)) + actual: AfterAccumulatedDepthElement = AfterAccumulatedDepthElement( + None, Decimal(0) + ) result = actual.process(context, model_state, [order]) # At depth of 7, price should be 75, not 74 if it was placed after depth of 7 @@ -100,9 +125,13 @@ def test_bid_price_updated_at_instead_of_after() -> None: def test_ask_price_updated_at_instead_of_after() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(82), quantity=Decimal(6), side=mango.Side.SELL) + order: mango.Order = fake_order( + price=Decimal(82), quantity=Decimal(6), side=mango.Side.SELL + ) - actual: AfterAccumulatedDepthElement = AfterAccumulatedDepthElement(None, Decimal(0)) + actual: AfterAccumulatedDepthElement = AfterAccumulatedDepthElement( + None, Decimal(0) + ) result = actual.process(context, model_state, [order]) # At depth of 6, price should be 85, not 74 if it was placed after depth of 6 @@ -111,7 +140,9 @@ def test_ask_price_updated_at_instead_of_after() -> None: def test_bid_price_updated_for_fixed_depth() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(78), quantity=Decimal(7), side=mango.Side.BUY) + order: mango.Order = fake_order( + price=Decimal(78), quantity=Decimal(7), side=mango.Side.BUY + ) actual: AfterAccumulatedDepthElement = AfterAccumulatedDepthElement(Decimal(3)) result = actual.process(context, model_state, [order]) @@ -122,7 +153,9 @@ def test_bid_price_updated_for_fixed_depth() -> None: def test_ask_price_updated_for_fixed_depth() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(82), quantity=Decimal(6), side=mango.Side.SELL) + order: mango.Order = fake_order( + price=Decimal(82), quantity=Decimal(6), side=mango.Side.SELL + ) actual: AfterAccumulatedDepthElement = AfterAccumulatedDepthElement(Decimal(3)) result = actual.process(context, model_state, [order]) @@ -133,9 +166,13 @@ def test_ask_price_updated_for_fixed_depth() -> None: def test_bid_price_updated_at_instead_of_after_fixed_depth() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(78), quantity=Decimal(7), side=mango.Side.BUY) + order: mango.Order = fake_order( + price=Decimal(78), quantity=Decimal(7), side=mango.Side.BUY + ) - actual: AfterAccumulatedDepthElement = AfterAccumulatedDepthElement(Decimal(3), Decimal(0)) + actual: AfterAccumulatedDepthElement = AfterAccumulatedDepthElement( + Decimal(3), Decimal(0) + ) result = actual.process(context, model_state, [order]) # At fixed depth of 3, price should be 77 (not 74 if depth was order quantity, or @@ -145,9 +182,13 @@ def test_bid_price_updated_at_instead_of_after_fixed_depth() -> None: def test_ask_price_updated_at_instead_of_after_fixed_depth() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(82), quantity=Decimal(6), side=mango.Side.SELL) + order: mango.Order = fake_order( + price=Decimal(82), quantity=Decimal(6), side=mango.Side.SELL + ) - actual: AfterAccumulatedDepthElement = AfterAccumulatedDepthElement(Decimal(3), Decimal(0)) + actual: AfterAccumulatedDepthElement = AfterAccumulatedDepthElement( + Decimal(3), Decimal(0) + ) result = actual.process(context, model_state, [order]) # At fixed depth of 3, price should be 82 (not 86 if depth was order quantity, or diff --git a/tests/marketmaking/orderchain/test_biasquantityonpositionelement.py b/tests/marketmaking/orderchain/test_biasquantityonpositionelement.py index c1d8551..f86033b 100644 --- a/tests/marketmaking/orderchain/test_biasquantityonpositionelement.py +++ b/tests/marketmaking/orderchain/test_biasquantityonpositionelement.py @@ -2,12 +2,20 @@ import argparse import typing from ...context import mango -from ...fakes import fake_context, fake_model_state, fake_order, fake_price, fake_inventory +from ...fakes import ( + fake_context, + fake_model_state, + fake_order, + fake_price, + fake_inventory, +) from dataclasses import dataclass from decimal import Decimal -from mango.marketmaking.orderchain.biasquantityonpositionelement import BiasQuantityOnPositionElement +from mango.marketmaking.orderchain.biasquantityonpositionelement import ( + BiasQuantityOnPositionElement, +) @dataclass() @@ -19,12 +27,20 @@ class BQOPInput: maximum: Decimal -def __biasquantityonposition_pair(input_data: BQOPInput) -> typing.Tuple[Decimal, Decimal]: - element: BiasQuantityOnPositionElement = BiasQuantityOnPositionElement(input_data.maximum, input_data.target) +def __biasquantityonposition_pair( + input_data: BQOPInput, +) -> typing.Tuple[Decimal, Decimal]: + element: BiasQuantityOnPositionElement = BiasQuantityOnPositionElement( + input_data.maximum, input_data.target + ) context = fake_context() - model_state = fake_model_state(price=fake_price(price=Decimal(100)), - inventory=fake_inventory(base=input_data.current)) - buy, sell = element.process_order_pair(context, model_state, 0, input_data.buy, input_data.sell) + model_state = fake_model_state( + price=fake_price(price=Decimal(100)), + inventory=fake_inventory(base=input_data.current), + ) + buy, sell = element.process_order_pair( + context, model_state, 0, input_data.buy, input_data.sell + ) if buy is None: raise Exception("BUY should never be None in this test") if sell is None: @@ -33,15 +49,21 @@ def __biasquantityonposition_pair(input_data: BQOPInput) -> typing.Tuple[Decimal def test_from_args() -> None: - args: argparse.Namespace = argparse.Namespace(biasquantityonposition_maximum_position=Decimal( - 17), biasquantityonposition_target_position=Decimal(7)) - actual: BiasQuantityOnPositionElement = BiasQuantityOnPositionElement.from_command_line_parameters(args) + args: argparse.Namespace = argparse.Namespace( + biasquantityonposition_maximum_position=Decimal(17), + biasquantityonposition_target_position=Decimal(7), + ) + actual: BiasQuantityOnPositionElement = ( + BiasQuantityOnPositionElement.from_command_line_parameters(args) + ) assert actual.maximum_position == 17 assert actual.target_position == 7 def test_constructor() -> None: - actual: BiasQuantityOnPositionElement = BiasQuantityOnPositionElement(Decimal(17), Decimal(7)) + actual: BiasQuantityOnPositionElement = BiasQuantityOnPositionElement( + Decimal(17), Decimal(7) + ) assert actual.maximum_position == 17 assert actual.target_position == 7 @@ -72,12 +94,41 @@ def test_table_target_zero() -> None: buy: mango.Order = fake_order(quantity=Decimal(10), side=mango.Side.BUY) sell: mango.Order = fake_order(quantity=Decimal(10), side=mango.Side.SELL) target: Decimal = Decimal(0) - positions: typing.Sequence[int] = [0, 10, 20, 30, 40, 50, 60, -10, -20, -30, -40, -50, -60] + positions: typing.Sequence[int] = [ + 0, + 10, + 20, + 30, + 40, + 50, + 60, + -10, + -20, + -30, + -40, + -50, + -60, + ] adjusted_buys: typing.Sequence[int] = [10, 8, 6, 4, 2, 0, 0, 12, 14, 16, 18, 20, 20] - adjusted_sells: typing.Sequence[int] = [10, 12, 14, 16, 18, 20, 20, 8, 6, 4, 2, 0, 0] + adjusted_sells: typing.Sequence[int] = [ + 10, + 12, + 14, + 16, + 18, + 20, + 20, + 8, + 6, + 4, + 2, + 0, + 0, + ] for index, position in enumerate(positions): buy_quantity, sell_quantity = __biasquantityonposition_pair( - BQOPInput(buy, sell, Decimal(position), target, Decimal(50))) + BQOPInput(buy, sell, Decimal(position), target, Decimal(50)) + ) assert buy_quantity == Decimal(adjusted_buys[index]) assert sell_quantity == Decimal(adjusted_sells[index]) @@ -96,7 +147,8 @@ def test_table_fractional_target_zero() -> None: adjusted_sells: typing.Sequence[Decimal] = [Decimal("0.375"), Decimal("0.125")] for index, position in enumerate(positions): buy_quantity, sell_quantity = __biasquantityonposition_pair( - BQOPInput(buy, sell, Decimal(position), target, Decimal(4))) + BQOPInput(buy, sell, Decimal(position), target, Decimal(4)) + ) assert buy_quantity == adjusted_buys[index] assert sell_quantity == adjusted_sells[index] @@ -121,12 +173,41 @@ def test_table_negative_target() -> None: buy: mango.Order = fake_order(quantity=Decimal(10), side=mango.Side.BUY) sell: mango.Order = fake_order(quantity=Decimal(10), side=mango.Side.SELL) target: Decimal = Decimal(-10) - positions: typing.Sequence[int] = [0, 10, 20, 30, 40, 50, 60, -10, -20, -30, -40, -50, -60] + positions: typing.Sequence[int] = [ + 0, + 10, + 20, + 30, + 40, + 50, + 60, + -10, + -20, + -30, + -40, + -50, + -60, + ] adjusted_buys: typing.Sequence[int] = [8, 6, 4, 2, 0, 0, 0, 10, 12, 14, 16, 18, 20] - adjusted_sells: typing.Sequence[int] = [12, 14, 16, 18, 20, 20, 20, 10, 8, 6, 4, 2, 0] + adjusted_sells: typing.Sequence[int] = [ + 12, + 14, + 16, + 18, + 20, + 20, + 20, + 10, + 8, + 6, + 4, + 2, + 0, + ] for index, position in enumerate(positions): buy_quantity, sell_quantity = __biasquantityonposition_pair( - BQOPInput(buy, sell, Decimal(position), target, Decimal(50))) + BQOPInput(buy, sell, Decimal(position), target, Decimal(50)) + ) assert buy_quantity == Decimal(adjusted_buys[index]) assert sell_quantity == Decimal(adjusted_sells[index]) @@ -148,13 +229,32 @@ def test_table_fractional_negative_target() -> None: sell: mango.Order = fake_order(quantity=Decimal("0.25"), side=mango.Side.SELL) target: Decimal = Decimal(-2) positions: typing.Sequence[int] = [0, 1, 2, 3, 4, -1, -2, -3, -4] - adjusted_buys: typing.Sequence[Decimal] = [Decimal("0.125"), Decimal("0.0625"), Decimal(0), Decimal( - 0), Decimal(0), Decimal("0.1875"), Decimal("0.25"), Decimal("0.3125"), Decimal("0.375")] - adjusted_sells: typing.Sequence[Decimal] = [Decimal("0.375"), Decimal("0.4375"), Decimal("0.5"), Decimal( - "0.5"), Decimal("0.5"), Decimal("0.3125"), Decimal("0.25"), Decimal("0.1875"), Decimal("0.125")] + adjusted_buys: typing.Sequence[Decimal] = [ + Decimal("0.125"), + Decimal("0.0625"), + Decimal(0), + Decimal(0), + Decimal(0), + Decimal("0.1875"), + Decimal("0.25"), + Decimal("0.3125"), + Decimal("0.375"), + ] + adjusted_sells: typing.Sequence[Decimal] = [ + Decimal("0.375"), + Decimal("0.4375"), + Decimal("0.5"), + Decimal("0.5"), + Decimal("0.5"), + Decimal("0.3125"), + Decimal("0.25"), + Decimal("0.1875"), + Decimal("0.125"), + ] for index, position in enumerate(positions): buy_quantity, sell_quantity = __biasquantityonposition_pair( - BQOPInput(buy, sell, Decimal(position), target, Decimal(4))) + BQOPInput(buy, sell, Decimal(position), target, Decimal(4)) + ) assert buy_quantity == adjusted_buys[index] assert sell_quantity == adjusted_sells[index] @@ -176,12 +276,31 @@ def test_table_fractional_positive_target() -> None: sell: mango.Order = fake_order(quantity=Decimal("0.25"), side=mango.Side.SELL) target: Decimal = Decimal(2) positions: typing.Sequence[int] = [0, 1, 2, 3, 4, -1, -2, -3, -4] - adjusted_buys: typing.Sequence[Decimal] = [Decimal("0.375"), Decimal("0.3125"), Decimal("0.25"), Decimal( - "0.1875"), Decimal("0.125"), Decimal("0.4375"), Decimal("0.5"), Decimal("0.5"), Decimal("0.5")] - adjusted_sells: typing.Sequence[Decimal] = [Decimal("0.125"), Decimal("0.1875"), Decimal("0.25"), Decimal( - "0.3125"), Decimal("0.375"), Decimal("0.0625"), Decimal("0"), Decimal("0"), Decimal("0")] + adjusted_buys: typing.Sequence[Decimal] = [ + Decimal("0.375"), + Decimal("0.3125"), + Decimal("0.25"), + Decimal("0.1875"), + Decimal("0.125"), + Decimal("0.4375"), + Decimal("0.5"), + Decimal("0.5"), + Decimal("0.5"), + ] + adjusted_sells: typing.Sequence[Decimal] = [ + Decimal("0.125"), + Decimal("0.1875"), + Decimal("0.25"), + Decimal("0.3125"), + Decimal("0.375"), + Decimal("0.0625"), + Decimal("0"), + Decimal("0"), + Decimal("0"), + ] for index, position in enumerate(positions): buy_quantity, sell_quantity = __biasquantityonposition_pair( - BQOPInput(buy, sell, Decimal(position), target, Decimal(4))) + BQOPInput(buy, sell, Decimal(position), target, Decimal(4)) + ) assert buy_quantity == adjusted_buys[index] assert sell_quantity == adjusted_sells[index] diff --git a/tests/marketmaking/orderchain/test_biasquoteelement.py b/tests/marketmaking/orderchain/test_biasquoteelement.py index 64b9f3e..62d42c8 100644 --- a/tests/marketmaking/orderchain/test_biasquoteelement.py +++ b/tests/marketmaking/orderchain/test_biasquoteelement.py @@ -67,7 +67,9 @@ def test_single_factor_greater_than_one() -> None: def test_single_factor_less_than_one() -> None: context = fake_context() model_state = fake_model_state(price=fake_price(price=Decimal(100))) - actual: BiasQuoteElement = BiasQuoteElement([Decimal("0.999")]) # shift 10 bips down + actual: BiasQuoteElement = BiasQuoteElement( + [Decimal("0.999")] + ) # shift 10 bips down buy: mango.Order = fake_order(price=Decimal(90), side=mango.Side.BUY) sell: mango.Order = fake_order(price=Decimal(110), side=mango.Side.SELL) @@ -80,7 +82,9 @@ def test_single_factor_less_than_one() -> None: def test_single_factor_two_order_pairs() -> None: context = fake_context() model_state = fake_model_state(price=fake_price(price=Decimal(100))) - actual: BiasQuoteElement = BiasQuoteElement([Decimal("0.999")]) # shift 10 bips down + actual: BiasQuoteElement = BiasQuoteElement( + [Decimal("0.999")] + ) # shift 10 bips down buy1: mango.Order = fake_order(price=Decimal(80), side=mango.Side.BUY) buy2: mango.Order = fake_order(price=Decimal(90), side=mango.Side.BUY) sell1: mango.Order = fake_order(price=Decimal(110), side=mango.Side.SELL) @@ -98,7 +102,9 @@ def test_single_factor_two_order_pairs() -> None: def test_two_factors_two_order_pairs() -> None: context = fake_context() model_state = fake_model_state(price=fake_price(price=Decimal(100))) - actual: BiasQuoteElement = BiasQuoteElement([Decimal("0.999"), Decimal("0.9")]) # shift 10 bips down + actual: BiasQuoteElement = BiasQuoteElement( + [Decimal("0.999"), Decimal("0.9")] + ) # shift 10 bips down buy1: mango.Order = fake_order(price=Decimal(80), side=mango.Side.BUY) buy2: mango.Order = fake_order(price=Decimal(90), side=mango.Side.BUY) sell1: mango.Order = fake_order(price=Decimal(110), side=mango.Side.SELL) @@ -116,7 +122,9 @@ def test_two_factors_two_order_pairs() -> None: def test_three_factors_three_order_pairs() -> None: context = fake_context() model_state = fake_model_state(price=fake_price(price=Decimal(100))) - actual: BiasQuoteElement = BiasQuoteElement([Decimal("0.9"), Decimal("0.8"), Decimal("0.7")]) # shift 10 bips down + actual: BiasQuoteElement = BiasQuoteElement( + [Decimal("0.9"), Decimal("0.8"), Decimal("0.7")] + ) # shift 10 bips down buy1: mango.Order = fake_order(price=Decimal(70), side=mango.Side.BUY) buy2: mango.Order = fake_order(price=Decimal(80), side=mango.Side.BUY) buy3: mango.Order = fake_order(price=Decimal(90), side=mango.Side.BUY) @@ -124,7 +132,9 @@ def test_three_factors_three_order_pairs() -> None: sell2: mango.Order = fake_order(price=Decimal(120), side=mango.Side.SELL) sell3: mango.Order = fake_order(price=Decimal(130), side=mango.Side.SELL) - result = actual.process(context, model_state, [buy1, buy2, buy3, sell1, sell2, sell3]) + result = actual.process( + context, model_state, [buy1, buy2, buy3, sell1, sell2, sell3] + ) # Should be re-ordered as closest to top-of-book, so buy3-sell1 then buy2-sell2, then buy1-sell3 assert result[0].price == Decimal("81") # 90 * 0.9 = 81 diff --git a/tests/marketmaking/orderchain/test_biasquoteonpositionelement.py b/tests/marketmaking/orderchain/test_biasquoteonpositionelement.py index e2b57d5..5748366 100644 --- a/tests/marketmaking/orderchain/test_biasquoteonpositionelement.py +++ b/tests/marketmaking/orderchain/test_biasquoteonpositionelement.py @@ -1,22 +1,36 @@ import argparse from ...context import mango -from ...fakes import fake_context, fake_inventory, fake_model_state, fake_order, fake_price +from ...fakes import ( + fake_context, + fake_inventory, + fake_model_state, + fake_order, + fake_price, +) from decimal import Decimal -from mango.marketmaking.orderchain.biasquoteonpositionelement import BiasQuoteOnPositionElement +from mango.marketmaking.orderchain.biasquoteonpositionelement import ( + BiasQuoteOnPositionElement, +) def test_from_args() -> None: - args: argparse.Namespace = argparse.Namespace(biasquoteonposition_bias=[Decimal(17)]) - actual: BiasQuoteOnPositionElement = BiasQuoteOnPositionElement.from_command_line_parameters(args) + args: argparse.Namespace = argparse.Namespace( + biasquoteonposition_bias=[Decimal(17)] + ) + actual: BiasQuoteOnPositionElement = ( + BiasQuoteOnPositionElement.from_command_line_parameters(args) + ) assert actual.biases == [17] # type: ignore[comparison-overlap] def test_no_bias_results_in_no_change() -> None: actual: BiasQuoteOnPositionElement = BiasQuoteOnPositionElement([]) - order: mango.Order = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10)) + order: mango.Order = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(1), quantity=Decimal(10) + ) result = actual.bias_order(order, Decimal(0), Decimal(100)) @@ -27,7 +41,9 @@ def test_bias_with_positive_inventory() -> None: quantity: Decimal = Decimal(10) inventory: Decimal = Decimal(100) actual: BiasQuoteOnPositionElement = BiasQuoteOnPositionElement([]) - order: mango.Order = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1000), quantity=quantity) + order: mango.Order = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(1000), quantity=quantity + ) bias: Decimal = Decimal("0.001") result = actual.bias_order(order, bias, inventory) @@ -43,7 +59,9 @@ def test_bias_with_negative_inventory() -> None: quantity: Decimal = Decimal(10) inventory: Decimal = Decimal(-100) actual: BiasQuoteOnPositionElement = BiasQuoteOnPositionElement([]) - order: mango.Order = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1000), quantity=quantity) + order: mango.Order = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(1000), quantity=quantity + ) bias: Decimal = Decimal("0.001") result = actual.bias_order(order, bias, inventory) @@ -68,7 +86,9 @@ def test_from_daffys_original_note() -> None: quantity: Decimal = Decimal("0.0002") inventory: Decimal = Decimal("0.0010") actual: BiasQuoteOnPositionElement = BiasQuoteOnPositionElement([]) - order: mango.Order = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(50000), quantity=quantity) + order: mango.Order = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(50000), quantity=quantity + ) bias: Decimal = Decimal("0.0001") result = actual.bias_order(order, bias, inventory) @@ -80,12 +100,22 @@ def test_from_daffys_original_note() -> None: def test_single_bias_two_order_pairs() -> None: context = fake_context() - model_state = fake_model_state(price=fake_price(price=Decimal(100)), inventory=fake_inventory()) + model_state = fake_model_state( + price=fake_price(price=Decimal(100)), inventory=fake_inventory() + ) actual: BiasQuoteOnPositionElement = BiasQuoteOnPositionElement([Decimal("0.001")]) - buy1: mango.Order = fake_order(price=Decimal(80), quantity=Decimal(1), side=mango.Side.BUY) - buy2: mango.Order = fake_order(price=Decimal(90), quantity=Decimal(2), side=mango.Side.BUY) - sell1: mango.Order = fake_order(price=Decimal(110), quantity=Decimal(2), side=mango.Side.SELL) - sell2: mango.Order = fake_order(price=Decimal(120), quantity=Decimal(1), side=mango.Side.SELL) + buy1: mango.Order = fake_order( + price=Decimal(80), quantity=Decimal(1), side=mango.Side.BUY + ) + buy2: mango.Order = fake_order( + price=Decimal(90), quantity=Decimal(2), side=mango.Side.BUY + ) + sell1: mango.Order = fake_order( + price=Decimal(110), quantity=Decimal(2), side=mango.Side.SELL + ) + sell2: mango.Order = fake_order( + price=Decimal(120), quantity=Decimal(1), side=mango.Side.SELL + ) result = actual.process(context, model_state, [buy1, buy2, sell1, sell2]) @@ -93,30 +123,51 @@ def test_single_bias_two_order_pairs() -> None: # Formula: # price * (1 + (curr_pos / size) * pos_lean) assert result[0].price == Decimal("89.55") # 90 * (1 + (10 / 2) * -0.001) == 89.55 - assert result[1].price == Decimal("109.45") # 110 * (1 + (10 / 2) * -0.001) == 109.45 + assert result[1].price == Decimal( + "109.45" + ) # 110 * (1 + (10 / 2) * -0.001) == 109.45 assert result[2].price == Decimal("79.2") # 80 * (1 + (10 / 1) * -0.001) == 79.2 assert result[3].price == Decimal("118.8") # 120 * (1 + (10 / 1) * -0.001) == 118.8 def test_three_biases_three_order_pairs() -> None: context = fake_context() - model_state = fake_model_state(price=fake_price(price=Decimal(100)), inventory=fake_inventory()) + model_state = fake_model_state( + price=fake_price(price=Decimal(100)), inventory=fake_inventory() + ) actual: BiasQuoteOnPositionElement = BiasQuoteOnPositionElement( - [Decimal("0.001"), Decimal("0.002"), Decimal("0.003")]) - buy1: mango.Order = fake_order(price=Decimal(70), quantity=Decimal(1), side=mango.Side.BUY) - buy2: mango.Order = fake_order(price=Decimal(80), quantity=Decimal(2), side=mango.Side.BUY) - buy3: mango.Order = fake_order(price=Decimal(90), quantity=Decimal(3), side=mango.Side.BUY) - sell1: mango.Order = fake_order(price=Decimal(110), quantity=Decimal(3), side=mango.Side.SELL) - sell2: mango.Order = fake_order(price=Decimal(120), quantity=Decimal(2), side=mango.Side.SELL) - sell3: mango.Order = fake_order(price=Decimal(130), quantity=Decimal(1), side=mango.Side.SELL) + [Decimal("0.001"), Decimal("0.002"), Decimal("0.003")] + ) + buy1: mango.Order = fake_order( + price=Decimal(70), quantity=Decimal(1), side=mango.Side.BUY + ) + buy2: mango.Order = fake_order( + price=Decimal(80), quantity=Decimal(2), side=mango.Side.BUY + ) + buy3: mango.Order = fake_order( + price=Decimal(90), quantity=Decimal(3), side=mango.Side.BUY + ) + sell1: mango.Order = fake_order( + price=Decimal(110), quantity=Decimal(3), side=mango.Side.SELL + ) + sell2: mango.Order = fake_order( + price=Decimal(120), quantity=Decimal(2), side=mango.Side.SELL + ) + sell3: mango.Order = fake_order( + price=Decimal(130), quantity=Decimal(1), side=mango.Side.SELL + ) - result = actual.process(context, model_state, [buy1, buy2, buy3, sell1, sell2, sell3]) + result = actual.process( + context, model_state, [buy1, buy2, buy3, sell1, sell2, sell3] + ) # Should be re-ordered as closest to top-of-book, so buy3-sell1 then buy2-sell2 then buy1-sell3 # Formula: # price * (1 + (curr_pos / size) * pos_lean) assert result[0].price == Decimal("89.7") # 90 * (1 + (10 / 3) * -0.001) == 89.7 - assert f"{result[1].price:.8f}" == "109.63333333" # 110 * (1 + (10 / 3) * -0.001) == 109.6333333333 + assert ( + f"{result[1].price:.8f}" == "109.63333333" + ) # 110 * (1 + (10 / 3) * -0.001) == 109.6333333333 assert result[2].price == Decimal("79.2") # 80 * (1 + (10 / 2) * -0.002) == 79.2 assert result[3].price == Decimal("118.8") # 120 * (1 + (10 / 2) * -0.002) == 118.8 assert result[4].price == Decimal("67.9") # 70 * (1 + (10 / 1) * -0.003) == 67.9 diff --git a/tests/marketmaking/orderchain/test_fixedpositionsizeelement.py b/tests/marketmaking/orderchain/test_fixedpositionsizeelement.py index 65a4a14..d0b08aa 100644 --- a/tests/marketmaking/orderchain/test_fixedpositionsizeelement.py +++ b/tests/marketmaking/orderchain/test_fixedpositionsizeelement.py @@ -5,7 +5,9 @@ from ...fakes import fake_context, fake_model_state, fake_order, fake_price from decimal import Decimal -from mango.marketmaking.orderchain.fixedpositionsizeelement import FixedPositionSizeElement +from mango.marketmaking.orderchain.fixedpositionsizeelement import ( + FixedPositionSizeElement, +) model_state = fake_model_state(price=fake_price(price=Decimal(80))) @@ -13,7 +15,9 @@ model_state = fake_model_state(price=fake_price(price=Decimal(80))) def test_from_args() -> None: args: argparse.Namespace = argparse.Namespace(fixedpositionsize_value=[Decimal(17)]) - actual: FixedPositionSizeElement = FixedPositionSizeElement.from_command_line_parameters(args) + actual: FixedPositionSizeElement = ( + FixedPositionSizeElement.from_command_line_parameters(args) + ) assert actual.position_sizes == [17] # type: ignore[comparison-overlap] @@ -62,11 +66,15 @@ def test_three_quantities_six_paired_orders_different_order_updated() -> None: order5: mango.Order = fake_order(quantity=Decimal(12), side=mango.Side.SELL) order6: mango.Order = fake_order(quantity=Decimal(13), side=mango.Side.SELL) - actual: FixedPositionSizeElement = FixedPositionSizeElement([Decimal(22), Decimal(33), Decimal(44)]) + actual: FixedPositionSizeElement = FixedPositionSizeElement( + [Decimal(22), Decimal(33), Decimal(44)] + ) # This line is different from previous test - orders are in different order but should be # returned in the proper order - result = actual.process(context, model_state, [order4, order3, order1, order2, order6, order5]) + result = actual.process( + context, model_state, [order4, order3, order1, order2, order6, order5] + ) assert result[0].quantity == 22 assert result[1].quantity == 22 @@ -85,8 +93,12 @@ def test_two_quantities_six_paired_orders_different_order_updated() -> None: order5: mango.Order = fake_order(quantity=Decimal(12), side=mango.Side.SELL) order6: mango.Order = fake_order(quantity=Decimal(13), side=mango.Side.SELL) - actual: FixedPositionSizeElement = FixedPositionSizeElement([Decimal(22), Decimal(33)]) - result = actual.process(context, model_state, [order4, order3, order1, order2, order6, order5]) + actual: FixedPositionSizeElement = FixedPositionSizeElement( + [Decimal(22), Decimal(33)] + ) + result = actual.process( + context, model_state, [order4, order3, order1, order2, order6, order5] + ) assert result[0].quantity == 22 assert result[1].quantity == 22 diff --git a/tests/marketmaking/orderchain/test_fixedspreadelement.py b/tests/marketmaking/orderchain/test_fixedspreadelement.py index 4c5b540..602a0a2 100644 --- a/tests/marketmaking/orderchain/test_fixedspreadelement.py +++ b/tests/marketmaking/orderchain/test_fixedspreadelement.py @@ -59,8 +59,12 @@ def test_three_spreads_six_paired_orders_different_order_updated() -> None: order5: mango.Order = fake_order(price=Decimal(12), side=mango.Side.SELL) order6: mango.Order = fake_order(price=Decimal(13), side=mango.Side.SELL) - actual: FixedSpreadElement = FixedSpreadElement([Decimal(4), Decimal(6), Decimal(8)]) - result = actual.process(context, model_state, [order4, order3, order1, order2, order6, order5]) + actual: FixedSpreadElement = FixedSpreadElement( + [Decimal(4), Decimal(6), Decimal(8)] + ) + result = actual.process( + context, model_state, [order4, order3, order1, order2, order6, order5] + ) assert result[0].price == 8 assert result[1].price == 12 @@ -81,7 +85,9 @@ def test_two_spreads_six_paired_orders_different_order_updated() -> None: order6: mango.Order = fake_order(price=Decimal(13), side=mango.Side.SELL) actual: FixedSpreadElement = FixedSpreadElement([Decimal(4), Decimal(6)]) - result = actual.process(context, model_state, [order4, order3, order1, order2, order6, order5]) + result = actual.process( + context, model_state, [order4, order3, order1, order2, order6, order5] + ) assert result[0].price == 8 assert result[1].price == 12 diff --git a/tests/marketmaking/orderchain/test_maximumpositionsizeelement.py b/tests/marketmaking/orderchain/test_maximumpositionsizeelement.py index 27c3be1..d37fc01 100644 --- a/tests/marketmaking/orderchain/test_maximumpositionsizeelement.py +++ b/tests/marketmaking/orderchain/test_maximumpositionsizeelement.py @@ -14,7 +14,7 @@ bids: typing.Sequence[mango.Order] = [ fake_order(price=Decimal(76), quantity=Decimal(1), side=mango.Side.BUY), fake_order(price=Decimal(75), quantity=Decimal(5), side=mango.Side.BUY), fake_order(price=Decimal(74), quantity=Decimal(3), side=mango.Side.BUY), - fake_order(price=Decimal(73), quantity=Decimal(7), side=mango.Side.BUY) + fake_order(price=Decimal(73), quantity=Decimal(7), side=mango.Side.BUY), ] asks: typing.Sequence[mango.Order] = [ fake_order(price=Decimal(82), quantity=Decimal(3), side=mango.Side.SELL), @@ -22,17 +22,21 @@ asks: typing.Sequence[mango.Order] = [ fake_order(price=Decimal(84), quantity=Decimal(1), side=mango.Side.SELL), fake_order(price=Decimal(85), quantity=Decimal(3), side=mango.Side.SELL), fake_order(price=Decimal(86), quantity=Decimal(3), side=mango.Side.SELL), - fake_order(price=Decimal(87), quantity=Decimal(7), side=mango.Side.SELL) + fake_order(price=Decimal(87), quantity=Decimal(7), side=mango.Side.SELL), ] -orderbook: mango.OrderBook = mango.OrderBook("TEST", mango.NullLotSizeConverter(), bids, asks) +orderbook: mango.OrderBook = mango.OrderBook( + "TEST", mango.NullLotSizeConverter(), bids, asks +) model_state = fake_model_state(orderbook=orderbook) def test_from_args() -> None: args: argparse.Namespace = argparse.Namespace( - maximumquantity_size=Decimal(7), - maximumquantity_remove=True) - actual: MaximumQuantityElement = MaximumQuantityElement.from_command_line_parameters(args) + maximumquantity_size=Decimal(7), maximumquantity_remove=True + ) + actual: MaximumQuantityElement = ( + MaximumQuantityElement.from_command_line_parameters(args) + ) assert actual is not None assert actual.maximum_quantity == 7 assert actual.remove @@ -40,7 +44,9 @@ def test_from_args() -> None: def test_low_buy_not_updated() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(75), quantity=Decimal(7), side=mango.Side.BUY) + order: mango.Order = fake_order( + price=Decimal(75), quantity=Decimal(7), side=mango.Side.BUY + ) actual: MaximumQuantityElement = MaximumQuantityElement(Decimal(10)) result = actual.process(context, model_state, [order]) @@ -50,7 +56,9 @@ def test_low_buy_not_updated() -> None: def test_high_buy_updated() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(75), quantity=Decimal(17), side=mango.Side.BUY) + order: mango.Order = fake_order( + price=Decimal(75), quantity=Decimal(17), side=mango.Side.BUY + ) actual: MaximumQuantityElement = MaximumQuantityElement(Decimal(10)) result = actual.process(context, model_state, [order]) @@ -60,7 +68,9 @@ def test_high_buy_updated() -> None: def test_high_buy_removed() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(75), quantity=Decimal(17), side=mango.Side.BUY) + order: mango.Order = fake_order( + price=Decimal(75), quantity=Decimal(17), side=mango.Side.BUY + ) actual: MaximumQuantityElement = MaximumQuantityElement(Decimal(10), True) result = actual.process(context, model_state, [order]) @@ -70,7 +80,9 @@ def test_high_buy_removed() -> None: def test_low_sell_not_updated() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(85), quantity=Decimal(6), side=mango.Side.SELL) + order: mango.Order = fake_order( + price=Decimal(85), quantity=Decimal(6), side=mango.Side.SELL + ) actual: MaximumQuantityElement = MaximumQuantityElement(Decimal(10)) result = actual.process(context, model_state, [order]) @@ -80,7 +92,9 @@ def test_low_sell_not_updated() -> None: def test_high_sell_updated() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(85), quantity=Decimal(16), side=mango.Side.SELL) + order: mango.Order = fake_order( + price=Decimal(85), quantity=Decimal(16), side=mango.Side.SELL + ) actual: MaximumQuantityElement = MaximumQuantityElement(Decimal(10)) result = actual.process(context, model_state, [order]) @@ -90,7 +104,9 @@ def test_high_sell_updated() -> None: def test_high_sell_removed() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(85), quantity=Decimal(16), side=mango.Side.SELL) + order: mango.Order = fake_order( + price=Decimal(85), quantity=Decimal(16), side=mango.Side.SELL + ) actual: MaximumQuantityElement = MaximumQuantityElement(Decimal(10), True) result = actual.process(context, model_state, [order]) diff --git a/tests/marketmaking/orderchain/test_minimumchargeelement.py b/tests/marketmaking/orderchain/test_minimumchargeelement.py index f5a803f..2210a2a 100644 --- a/tests/marketmaking/orderchain/test_minimumchargeelement.py +++ b/tests/marketmaking/orderchain/test_minimumchargeelement.py @@ -8,12 +8,18 @@ from decimal import Decimal from mango.marketmaking.orderchain.minimumchargeelement import MinimumChargeElement -model_state = fake_model_state(price=fake_price(bid=Decimal(75), price=Decimal(80), ask=Decimal(85))) +model_state = fake_model_state( + price=fake_price(bid=Decimal(75), price=Decimal(80), ask=Decimal(85)) +) def test_from_args() -> None: - args: argparse.Namespace = argparse.Namespace(minimumcharge_ratio=[Decimal("0.2")], minimumcharge_from_bid_ask=True) - actual: MinimumChargeElement = MinimumChargeElement.from_command_line_parameters(args) + args: argparse.Namespace = argparse.Namespace( + minimumcharge_ratio=[Decimal("0.2")], minimumcharge_from_bid_ask=True + ) + actual: MinimumChargeElement = MinimumChargeElement.from_command_line_parameters( + args + ) assert actual.minimumcharge_ratios == [Decimal("0.2")] assert actual.minimumcharge_from_bid_ask @@ -120,7 +126,9 @@ def test_ask_price_lower_than_mid() -> None: def test_sol_bid_price_updated() -> None: context = fake_context() - model_state = fake_model_state(price=fake_price(bid=Decimal(181.9), price=Decimal(182), ask=Decimal(182.1))) + model_state = fake_model_state( + price=fake_price(bid=Decimal(181.9), price=Decimal(182), ask=Decimal(182.1)) + ) order: mango.Order = fake_order(price=Decimal("181.91"), side=mango.Side.BUY) actual: MinimumChargeElement = MinimumChargeElement([Decimal("0.0005")], False) @@ -131,7 +139,9 @@ def test_sol_bid_price_updated() -> None: def test_sol_ask_price_updated() -> None: context = fake_context() - model_state = fake_model_state(price=fake_price(bid=Decimal(181.9), price=Decimal(182), ask=Decimal(182.1))) + model_state = fake_model_state( + price=fake_price(bid=Decimal(181.9), price=Decimal(182), ask=Decimal(182.1)) + ) order: mango.Order = fake_order(price=Decimal("182.09"), side=mango.Side.SELL) actual: MinimumChargeElement = MinimumChargeElement([Decimal("0.0005")], False) @@ -143,7 +153,9 @@ def test_sol_ask_price_updated() -> None: def test_two_minimum_charges_two_order_pairs() -> None: context = fake_context() model_state = fake_model_state(price=fake_price(price=Decimal(100))) - actual: MinimumChargeElement = MinimumChargeElement([Decimal("0.1"), Decimal("0.2")], False) + actual: MinimumChargeElement = MinimumChargeElement( + [Decimal("0.1"), Decimal("0.2")], False + ) buy1: mango.Order = fake_order(price=Decimal(98), side=mango.Side.BUY) buy2: mango.Order = fake_order(price=Decimal(99), side=mango.Side.BUY) sell1: mango.Order = fake_order(price=Decimal(101), side=mango.Side.SELL) @@ -161,7 +173,9 @@ def test_two_minimum_charges_two_order_pairs() -> None: def test_three_minimum_charges_three_order_pairs() -> None: context = fake_context() model_state = fake_model_state(price=fake_price(price=Decimal(100))) - actual: MinimumChargeElement = MinimumChargeElement([Decimal("0.1"), Decimal("0.2"), Decimal("0.3")], False) + actual: MinimumChargeElement = MinimumChargeElement( + [Decimal("0.1"), Decimal("0.2"), Decimal("0.3")], False + ) buy1: mango.Order = fake_order(price=Decimal(97), side=mango.Side.BUY) buy2: mango.Order = fake_order(price=Decimal(98), side=mango.Side.BUY) buy3: mango.Order = fake_order(price=Decimal(99), side=mango.Side.BUY) @@ -169,7 +183,9 @@ def test_three_minimum_charges_three_order_pairs() -> None: sell2: mango.Order = fake_order(price=Decimal(102), side=mango.Side.SELL) sell3: mango.Order = fake_order(price=Decimal(103), side=mango.Side.SELL) - result = actual.process(context, model_state, [buy1, buy2, buy3, sell1, sell2, sell3]) + result = actual.process( + context, model_state, [buy1, buy2, buy3, sell1, sell2, sell3] + ) # Should be re-ordered as closest to top-of-book, so buy3-sell1 then buy2-sell2 then buy1-sell3 assert result[0].price == Decimal("90") # 100 - (100 * 0.1) = 90 @@ -191,7 +207,9 @@ def test_single_minimum_charge_three_order_pairs() -> None: sell2: mango.Order = fake_order(price=Decimal(102), side=mango.Side.SELL) sell3: mango.Order = fake_order(price=Decimal(103), side=mango.Side.SELL) - result = actual.process(context, model_state, [buy1, buy2, buy3, sell1, sell2, sell3]) + result = actual.process( + context, model_state, [buy1, buy2, buy3, sell1, sell2, sell3] + ) # Should be re-ordered as closest to top-of-book, so buy3-sell1 then buy2-sell2 then buy1-sell3 assert result[0].price == Decimal("90") # 100 - (100 * 0.1) = 90 diff --git a/tests/marketmaking/orderchain/test_minimumpositionsizeelement.py b/tests/marketmaking/orderchain/test_minimumpositionsizeelement.py index 2972c5b..e50c6ed 100644 --- a/tests/marketmaking/orderchain/test_minimumpositionsizeelement.py +++ b/tests/marketmaking/orderchain/test_minimumpositionsizeelement.py @@ -14,7 +14,7 @@ bids: typing.Sequence[mango.Order] = [ fake_order(price=Decimal(76), quantity=Decimal(1), side=mango.Side.BUY), fake_order(price=Decimal(75), quantity=Decimal(5), side=mango.Side.BUY), fake_order(price=Decimal(74), quantity=Decimal(3), side=mango.Side.BUY), - fake_order(price=Decimal(73), quantity=Decimal(7), side=mango.Side.BUY) + fake_order(price=Decimal(73), quantity=Decimal(7), side=mango.Side.BUY), ] asks: typing.Sequence[mango.Order] = [ fake_order(price=Decimal(82), quantity=Decimal(3), side=mango.Side.SELL), @@ -22,17 +22,21 @@ asks: typing.Sequence[mango.Order] = [ fake_order(price=Decimal(84), quantity=Decimal(1), side=mango.Side.SELL), fake_order(price=Decimal(85), quantity=Decimal(3), side=mango.Side.SELL), fake_order(price=Decimal(86), quantity=Decimal(3), side=mango.Side.SELL), - fake_order(price=Decimal(87), quantity=Decimal(7), side=mango.Side.SELL) + fake_order(price=Decimal(87), quantity=Decimal(7), side=mango.Side.SELL), ] -orderbook: mango.OrderBook = mango.OrderBook("TEST", mango.NullLotSizeConverter(), bids, asks) +orderbook: mango.OrderBook = mango.OrderBook( + "TEST", mango.NullLotSizeConverter(), bids, asks +) model_state = fake_model_state(orderbook=orderbook) def test_from_args() -> None: args: argparse.Namespace = argparse.Namespace( - minimumquantity_size=Decimal(7), - minimumquantity_remove=True) - actual: MinimumQuantityElement = MinimumQuantityElement.from_command_line_parameters(args) + minimumquantity_size=Decimal(7), minimumquantity_remove=True + ) + actual: MinimumQuantityElement = ( + MinimumQuantityElement.from_command_line_parameters(args) + ) assert actual is not None assert actual.minimum_quantity == 7 assert actual.remove @@ -40,7 +44,9 @@ def test_from_args() -> None: def test_high_buy_not_updated() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(75), quantity=Decimal(20), side=mango.Side.BUY) + order: mango.Order = fake_order( + price=Decimal(75), quantity=Decimal(20), side=mango.Side.BUY + ) actual: MinimumQuantityElement = MinimumQuantityElement(Decimal(10)) result = actual.process(context, model_state, [order]) @@ -50,7 +56,9 @@ def test_high_buy_not_updated() -> None: def test_low_buy_updated() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(75), quantity=Decimal(7), side=mango.Side.BUY) + order: mango.Order = fake_order( + price=Decimal(75), quantity=Decimal(7), side=mango.Side.BUY + ) actual: MinimumQuantityElement = MinimumQuantityElement(Decimal(10)) result = actual.process(context, model_state, [order]) @@ -60,7 +68,9 @@ def test_low_buy_updated() -> None: def test_low_buy_removed() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(75), quantity=Decimal(7), side=mango.Side.BUY) + order: mango.Order = fake_order( + price=Decimal(75), quantity=Decimal(7), side=mango.Side.BUY + ) actual: MinimumQuantityElement = MinimumQuantityElement(Decimal(10), True) result = actual.process(context, model_state, [order]) @@ -70,7 +80,9 @@ def test_low_buy_removed() -> None: def test_high_sell_not_updated() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(85), quantity=Decimal(16), side=mango.Side.SELL) + order: mango.Order = fake_order( + price=Decimal(85), quantity=Decimal(16), side=mango.Side.SELL + ) actual: MinimumQuantityElement = MinimumQuantityElement(Decimal(10)) result = actual.process(context, model_state, [order]) @@ -80,7 +92,9 @@ def test_high_sell_not_updated() -> None: def test_low_sell_updated() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(85), quantity=Decimal(6), side=mango.Side.SELL) + order: mango.Order = fake_order( + price=Decimal(85), quantity=Decimal(6), side=mango.Side.SELL + ) actual: MinimumQuantityElement = MinimumQuantityElement(Decimal(10)) result = actual.process(context, model_state, [order]) @@ -90,7 +104,9 @@ def test_low_sell_updated() -> None: def test_low_sell_removed() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(85), quantity=Decimal(6), side=mango.Side.SELL) + order: mango.Order = fake_order( + price=Decimal(85), quantity=Decimal(6), side=mango.Side.SELL + ) actual: MinimumQuantityElement = MinimumQuantityElement(Decimal(10), True) result = actual.process(context, model_state, [order]) diff --git a/tests/marketmaking/orderchain/test_preventpostonlycrossingbookelement.py b/tests/marketmaking/orderchain/test_preventpostonlycrossingbookelement.py index 0223f25..22642b8 100644 --- a/tests/marketmaking/orderchain/test_preventpostonlycrossingbookelement.py +++ b/tests/marketmaking/orderchain/test_preventpostonlycrossingbookelement.py @@ -5,28 +5,40 @@ from ...fakes import fake_context, fake_model_state, fake_loaded_market, fake_or from decimal import Decimal -from mango.marketmaking.orderchain.preventpostonlycrossingbookelement import PreventPostOnlyCrossingBookElement +from mango.marketmaking.orderchain.preventpostonlycrossingbookelement import ( + PreventPostOnlyCrossingBookElement, +) # The top bid is the highest price someone is willing to pay to BUY -top_bid: mango.Order = fake_order(price=Decimal(90), side=mango.Side.BUY, order_type=mango.OrderType.POST_ONLY) +top_bid: mango.Order = fake_order( + price=Decimal(90), side=mango.Side.BUY, order_type=mango.OrderType.POST_ONLY +) # The top ask is the lowest price someone is willing to pay to SELL -top_ask: mango.Order = fake_order(price=Decimal(110), side=mango.Side.SELL, order_type=mango.OrderType.POST_ONLY) +top_ask: mango.Order = fake_order( + price=Decimal(110), side=mango.Side.SELL, order_type=mango.OrderType.POST_ONLY +) -orderbook: mango.OrderBook = mango.OrderBook("TEST", mango.NullLotSizeConverter(), [top_bid], [top_ask]) +orderbook: mango.OrderBook = mango.OrderBook( + "TEST", mango.NullLotSizeConverter(), [top_bid], [top_ask] +) model_state = fake_model_state(market=fake_loaded_market(), orderbook=orderbook) def test_from_args() -> None: args: argparse.Namespace = argparse.Namespace() - actual: PreventPostOnlyCrossingBookElement = PreventPostOnlyCrossingBookElement.from_command_line_parameters(args) + actual: PreventPostOnlyCrossingBookElement = ( + PreventPostOnlyCrossingBookElement.from_command_line_parameters(args) + ) assert actual is not None def test_not_crossing_results_in_no_change() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(100), order_type=mango.OrderType.POST_ONLY) + order: mango.Order = fake_order( + price=Decimal(100), order_type=mango.OrderType.POST_ONLY + ) actual: PreventPostOnlyCrossingBookElement = PreventPostOnlyCrossingBookElement() result = actual.process(context, model_state, [order]) @@ -36,7 +48,9 @@ def test_not_crossing_results_in_no_change() -> None: def test_bid_too_high_results_in_new_bid() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(120), side=mango.Side.BUY, order_type=mango.OrderType.POST_ONLY) + order: mango.Order = fake_order( + price=Decimal(120), side=mango.Side.BUY, order_type=mango.OrderType.POST_ONLY + ) actual: PreventPostOnlyCrossingBookElement = PreventPostOnlyCrossingBookElement() result = actual.process(context, model_state, [order]) @@ -46,7 +60,9 @@ def test_bid_too_high_results_in_new_bid() -> None: def test_bid_too_low_results_in_no_change() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(80), side=mango.Side.BUY, order_type=mango.OrderType.POST_ONLY) + order: mango.Order = fake_order( + price=Decimal(80), side=mango.Side.BUY, order_type=mango.OrderType.POST_ONLY + ) actual: PreventPostOnlyCrossingBookElement = PreventPostOnlyCrossingBookElement() result = actual.process(context, model_state, [order]) @@ -56,7 +72,9 @@ def test_bid_too_low_results_in_no_change() -> None: def test_ask_too_low_results_in_new_ask() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(80), side=mango.Side.SELL, order_type=mango.OrderType.POST_ONLY) + order: mango.Order = fake_order( + price=Decimal(80), side=mango.Side.SELL, order_type=mango.OrderType.POST_ONLY + ) actual: PreventPostOnlyCrossingBookElement = PreventPostOnlyCrossingBookElement() result = actual.process(context, model_state, [order]) @@ -66,7 +84,9 @@ def test_ask_too_low_results_in_new_ask() -> None: def test_ask_too_high_results_in_no_change() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(120), side=mango.Side.SELL, order_type=mango.OrderType.POST_ONLY) + order: mango.Order = fake_order( + price=Decimal(120), side=mango.Side.SELL, order_type=mango.OrderType.POST_ONLY + ) actual: PreventPostOnlyCrossingBookElement = PreventPostOnlyCrossingBookElement() result = actual.process(context, model_state, [order]) @@ -76,10 +96,14 @@ def test_ask_too_high_results_in_no_change() -> None: def test_bid_too_high_no_bid_results_in_new_bid() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(120), side=mango.Side.BUY, order_type=mango.OrderType.POST_ONLY) + order: mango.Order = fake_order( + price=Decimal(120), side=mango.Side.BUY, order_type=mango.OrderType.POST_ONLY + ) actual: PreventPostOnlyCrossingBookElement = PreventPostOnlyCrossingBookElement() - orderbook: mango.OrderBook = mango.OrderBook("TEST", mango.NullLotSizeConverter(), [], [top_ask]) + orderbook: mango.OrderBook = mango.OrderBook( + "TEST", mango.NullLotSizeConverter(), [], [top_ask] + ) model_state = fake_model_state(market=fake_loaded_market(), orderbook=orderbook) result = actual.process(context, model_state, [order]) @@ -89,10 +113,14 @@ def test_bid_too_high_no_bid_results_in_new_bid() -> None: def test_ask_too_low_no_ask_results_in_new_ask() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(80), side=mango.Side.SELL, order_type=mango.OrderType.POST_ONLY) + order: mango.Order = fake_order( + price=Decimal(80), side=mango.Side.SELL, order_type=mango.OrderType.POST_ONLY + ) actual: PreventPostOnlyCrossingBookElement = PreventPostOnlyCrossingBookElement() - orderbook: mango.OrderBook = mango.OrderBook("TEST", mango.NullLotSizeConverter(), [top_bid], []) + orderbook: mango.OrderBook = mango.OrderBook( + "TEST", mango.NullLotSizeConverter(), [top_bid], [] + ) model_state = fake_model_state(market=fake_loaded_market(), orderbook=orderbook) result = actual.process(context, model_state, [order]) @@ -101,10 +129,14 @@ def test_ask_too_low_no_ask_results_in_new_ask() -> None: def test_ask_no_orderbook_results_in_no_change() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(120), side=mango.Side.SELL, order_type=mango.OrderType.POST_ONLY) + order: mango.Order = fake_order( + price=Decimal(120), side=mango.Side.SELL, order_type=mango.OrderType.POST_ONLY + ) actual: PreventPostOnlyCrossingBookElement = PreventPostOnlyCrossingBookElement() - orderbook: mango.OrderBook = mango.OrderBook("TEST", mango.NullLotSizeConverter(), [], []) + orderbook: mango.OrderBook = mango.OrderBook( + "TEST", mango.NullLotSizeConverter(), [], [] + ) model_state = fake_model_state(market=fake_loaded_market(), orderbook=orderbook) result = actual.process(context, model_state, [order]) @@ -113,10 +145,14 @@ def test_ask_no_orderbook_results_in_no_change() -> None: def test_bid_no_orderbook_results_in_no_change() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(80), side=mango.Side.BUY, order_type=mango.OrderType.POST_ONLY) + order: mango.Order = fake_order( + price=Decimal(80), side=mango.Side.BUY, order_type=mango.OrderType.POST_ONLY + ) actual: PreventPostOnlyCrossingBookElement = PreventPostOnlyCrossingBookElement() - orderbook: mango.OrderBook = mango.OrderBook("TEST", mango.NullLotSizeConverter(), [], []) + orderbook: mango.OrderBook = mango.OrderBook( + "TEST", mango.NullLotSizeConverter(), [], [] + ) model_state = fake_model_state(market=fake_loaded_market(), orderbook=orderbook) result = actual.process(context, model_state, [order]) diff --git a/tests/marketmaking/orderchain/test_ratios.py b/tests/marketmaking/orderchain/test_ratios.py index 17c9504..a4ed2b1 100644 --- a/tests/marketmaking/orderchain/test_ratios.py +++ b/tests/marketmaking/orderchain/test_ratios.py @@ -8,12 +8,18 @@ from decimal import Decimal from mango.marketmaking.orderchain.ratioselement import RatiosElement -model_state = fake_model_state(price=fake_price(bid=Decimal(75), price=Decimal(80), ask=Decimal(85))) +model_state = fake_model_state( + price=fake_price(bid=Decimal(75), price=Decimal(80), ask=Decimal(85)) +) def test_from_args() -> None: - args: argparse.Namespace = argparse.Namespace(ratios_spread=[Decimal("0.7")], ratios_position_size=[Decimal("0.27")], - order_type=mango.OrderType.IOC, ratios_from_bid_ask=True) + args: argparse.Namespace = argparse.Namespace( + ratios_spread=[Decimal("0.7")], + ratios_position_size=[Decimal("0.27")], + order_type=mango.OrderType.IOC, + ratios_from_bid_ask=True, + ) actual: RatiosElement = RatiosElement.from_command_line_parameters(args) assert actual.order_type == mango.OrderType.IOC assert actual.spread_ratios == [Decimal("0.7")] @@ -24,7 +30,9 @@ def test_from_args() -> None: def test_uses_specified_spread_ratio() -> None: context = fake_context() - actual: RatiosElement = RatiosElement(mango.OrderType.POST_ONLY, [Decimal("0.1")], [Decimal("0.01")], False) + actual: RatiosElement = RatiosElement( + mango.OrderType.POST_ONLY, [Decimal("0.1")], [Decimal("0.01")], False + ) result = actual.process(context, model_state, []) assert result[0].price == Decimal("72") @@ -36,7 +44,9 @@ def test_uses_specified_spread_ratio() -> None: def test_uses_specified_position_size_ratio() -> None: context = fake_context() - actual: RatiosElement = RatiosElement(mango.OrderType.POST_ONLY, [Decimal("0.01")], [Decimal("0.1")], False) + actual: RatiosElement = RatiosElement( + mango.OrderType.POST_ONLY, [Decimal("0.01")], [Decimal("0.1")], False + ) result = actual.process(context, model_state, []) assert result[0].price == Decimal("79.2") @@ -48,7 +58,9 @@ def test_uses_specified_position_size_ratio() -> None: def test_uses_specified_spread_and_position_size_ratio() -> None: context = fake_context() - actual: RatiosElement = RatiosElement(mango.OrderType.POST_ONLY, [Decimal("0.1")], [Decimal("0.1")], False) + actual: RatiosElement = RatiosElement( + mango.OrderType.POST_ONLY, [Decimal("0.1")], [Decimal("0.1")], False + ) result = actual.process(context, model_state, []) assert result[0].price == Decimal("72") @@ -60,7 +72,9 @@ def test_uses_specified_spread_and_position_size_ratio() -> None: def test_uses_specified_spread_and_position_size_ratio_from_bid_ask() -> None: context = fake_context() - actual: RatiosElement = RatiosElement(mango.OrderType.POST_ONLY, [Decimal("0.1")], [Decimal("0.1")], True) + actual: RatiosElement = RatiosElement( + mango.OrderType.POST_ONLY, [Decimal("0.1")], [Decimal("0.1")], True + ) result = actual.process(context, model_state, []) assert result[0].price == Decimal("67.5") diff --git a/tests/marketmaking/orderchain/test_roundtolotsizeelement.py b/tests/marketmaking/orderchain/test_roundtolotsizeelement.py index 4735bce..6d68fbb 100644 --- a/tests/marketmaking/orderchain/test_roundtolotsizeelement.py +++ b/tests/marketmaking/orderchain/test_roundtolotsizeelement.py @@ -10,7 +10,9 @@ from mango.marketmaking.orderchain.roundtolotsizeelement import RoundToLotSizeEl def test_from_args() -> None: args: argparse.Namespace = argparse.Namespace() - actual: RoundToLotSizeElement = RoundToLotSizeElement.from_command_line_parameters(args) + actual: RoundToLotSizeElement = RoundToLotSizeElement.from_command_line_parameters( + args + ) assert actual is not None assert isinstance(actual, RoundToLotSizeElement) @@ -42,7 +44,9 @@ def test_rounds_quantity() -> None: def test_rounds_price_and_quantity() -> None: context = fake_context() model_state = fake_model_state() - order: mango.Order = fake_order(price=Decimal("1.23456789"), quantity=Decimal("1.23456789")) + order: mango.Order = fake_order( + price=Decimal("1.23456789"), quantity=Decimal("1.23456789") + ) actual: RoundToLotSizeElement = RoundToLotSizeElement() result = actual.process(context, model_state, [order]) @@ -76,7 +80,9 @@ def test_removes_when_quantity_rounds_to_zero() -> None: def test_removes_when_price_and_quantity_round_to_zero() -> None: context = fake_context() model_state = fake_model_state() - order: mango.Order = fake_order(price=Decimal("0.0000001"), quantity=Decimal("0.0000001")) + order: mango.Order = fake_order( + price=Decimal("0.0000001"), quantity=Decimal("0.0000001") + ) actual: RoundToLotSizeElement = RoundToLotSizeElement() result = actual.process(context, model_state, [order]) diff --git a/tests/marketmaking/orderchain/test_singlesidedelement.py b/tests/marketmaking/orderchain/test_singlesidedelement.py index d26a0f1..27aa399 100644 --- a/tests/marketmaking/orderchain/test_singlesidedelement.py +++ b/tests/marketmaking/orderchain/test_singlesidedelement.py @@ -8,7 +8,9 @@ from mango.marketmaking.orderchain.quotesinglesideelement import QuoteSingleSide def test_from_args() -> None: args: argparse.Namespace = argparse.Namespace(quotesingleside_side=mango.Side.BUY) - actual: QuoteSingleSideElement = QuoteSingleSideElement.from_command_line_parameters(args) + actual: QuoteSingleSideElement = ( + QuoteSingleSideElement.from_command_line_parameters(args) + ) assert actual is not None assert isinstance(actual, QuoteSingleSideElement) @@ -44,7 +46,7 @@ def test_allow_all_buys_and_no_sells() -> None: fake_order(side=mango.Side.BUY), fake_order(side=mango.Side.SELL), fake_order(side=mango.Side.BUY), - fake_order(side=mango.Side.SELL) + fake_order(side=mango.Side.SELL), ] actual: QuoteSingleSideElement = QuoteSingleSideElement(mango.Side.BUY) @@ -63,7 +65,7 @@ def test_allow_all_sells_and_no_buys() -> None: fake_order(side=mango.Side.BUY), fake_order(side=mango.Side.SELL), fake_order(side=mango.Side.BUY), - fake_order(side=mango.Side.SELL) + fake_order(side=mango.Side.SELL), ] actual: QuoteSingleSideElement = QuoteSingleSideElement(mango.Side.SELL) @@ -83,7 +85,7 @@ def test_allow_all_buys_and_no_sells_different_pattern() -> None: fake_order(side=mango.Side.SELL), fake_order(side=mango.Side.SELL), fake_order(side=mango.Side.SELL), - fake_order(side=mango.Side.BUY) + fake_order(side=mango.Side.BUY), ] actual: QuoteSingleSideElement = QuoteSingleSideElement(mango.Side.BUY) diff --git a/tests/marketmaking/orderchain/test_topofbookelement.py b/tests/marketmaking/orderchain/test_topofbookelement.py index a066db2..a7711ce 100644 --- a/tests/marketmaking/orderchain/test_topofbookelement.py +++ b/tests/marketmaking/orderchain/test_topofbookelement.py @@ -15,7 +15,7 @@ bids: typing.Sequence[mango.Order] = [ fake_order(price=Decimal(76), quantity=Decimal(1), side=mango.Side.BUY), fake_order(price=Decimal(75), quantity=Decimal(5), side=mango.Side.BUY), fake_order(price=Decimal(74), quantity=Decimal(3), side=mango.Side.BUY), - fake_order(price=Decimal(73), quantity=Decimal(7), side=mango.Side.BUY) + fake_order(price=Decimal(73), quantity=Decimal(7), side=mango.Side.BUY), ] asks: typing.Sequence[mango.Order] = [ fake_order(price=Decimal(82), quantity=Decimal(3), side=mango.Side.SELL), @@ -23,9 +23,11 @@ asks: typing.Sequence[mango.Order] = [ fake_order(price=Decimal(84), quantity=Decimal(1), side=mango.Side.SELL), fake_order(price=Decimal(85), quantity=Decimal(3), side=mango.Side.SELL), fake_order(price=Decimal(86), quantity=Decimal(3), side=mango.Side.SELL), - fake_order(price=Decimal(87), quantity=Decimal(7), side=mango.Side.SELL) + fake_order(price=Decimal(87), quantity=Decimal(7), side=mango.Side.SELL), ] -orderbook: mango.OrderBook = mango.OrderBook("TEST", mango.NullLotSizeConverter(), bids, asks) +orderbook: mango.OrderBook = mango.OrderBook( + "TEST", mango.NullLotSizeConverter(), bids, asks +) model_state = fake_model_state(orderbook=orderbook) @@ -37,7 +39,9 @@ def test_from_args() -> None: def test_bid_price_updated() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(75), quantity=Decimal(7), side=mango.Side.BUY) + order: mango.Order = fake_order( + price=Decimal(75), quantity=Decimal(7), side=mango.Side.BUY + ) actual: TopOfBookElement = TopOfBookElement() result = actual.process(context, model_state, [order]) @@ -47,7 +51,9 @@ def test_bid_price_updated() -> None: def test_ask_price_updated() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(85), quantity=Decimal(6), side=mango.Side.SELL) + order: mango.Order = fake_order( + price=Decimal(85), quantity=Decimal(6), side=mango.Side.SELL + ) actual: TopOfBookElement = TopOfBookElement() result = actual.process(context, model_state, [order]) @@ -58,26 +64,40 @@ def test_ask_price_updated() -> None: def test_top_check_ignores_own_orders_updated() -> None: order_owner: PublicKey = fake_seeded_public_key("order owner") bids: typing.Sequence[mango.Order] = [ - fake_order(price=Decimal(78), quantity=Decimal(1), side=mango.Side.BUY).with_owner(order_owner), + fake_order( + price=Decimal(78), quantity=Decimal(1), side=mango.Side.BUY + ).with_owner(order_owner), fake_order(price=Decimal(77), quantity=Decimal(2), side=mango.Side.BUY), fake_order(price=Decimal(76), quantity=Decimal(1), side=mango.Side.BUY), - fake_order(price=Decimal(75), quantity=Decimal(5), side=mango.Side.BUY).with_owner(order_owner), + fake_order( + price=Decimal(75), quantity=Decimal(5), side=mango.Side.BUY + ).with_owner(order_owner), fake_order(price=Decimal(74), quantity=Decimal(3), side=mango.Side.BUY), - fake_order(price=Decimal(73), quantity=Decimal(7), side=mango.Side.BUY) + fake_order(price=Decimal(73), quantity=Decimal(7), side=mango.Side.BUY), ] asks: typing.Sequence[mango.Order] = [ - fake_order(price=Decimal(82), quantity=Decimal(3), side=mango.Side.SELL).with_owner(order_owner), + fake_order( + price=Decimal(82), quantity=Decimal(3), side=mango.Side.SELL + ).with_owner(order_owner), fake_order(price=Decimal(83), quantity=Decimal(1), side=mango.Side.SELL), fake_order(price=Decimal(84), quantity=Decimal(1), side=mango.Side.SELL), - fake_order(price=Decimal(85), quantity=Decimal(3), side=mango.Side.SELL).with_owner(order_owner), + fake_order( + price=Decimal(85), quantity=Decimal(3), side=mango.Side.SELL + ).with_owner(order_owner), fake_order(price=Decimal(86), quantity=Decimal(3), side=mango.Side.SELL), - fake_order(price=Decimal(87), quantity=Decimal(7), side=mango.Side.SELL) + fake_order(price=Decimal(87), quantity=Decimal(7), side=mango.Side.SELL), ] - orderbook: mango.OrderBook = mango.OrderBook("TEST", mango.NullLotSizeConverter(), bids, asks) + orderbook: mango.OrderBook = mango.OrderBook( + "TEST", mango.NullLotSizeConverter(), bids, asks + ) model_state = fake_model_state(order_owner=order_owner, orderbook=orderbook) context = fake_context() - buy: mango.Order = fake_order(price=Decimal(75), quantity=Decimal(6), side=mango.Side.BUY) - sell: mango.Order = fake_order(price=Decimal(85), quantity=Decimal(6), side=mango.Side.SELL) + buy: mango.Order = fake_order( + price=Decimal(75), quantity=Decimal(6), side=mango.Side.BUY + ) + sell: mango.Order = fake_order( + price=Decimal(85), quantity=Decimal(6), side=mango.Side.SELL + ) actual: TopOfBookElement = TopOfBookElement() result = actual.process(context, model_state, [buy, sell]) @@ -88,7 +108,9 @@ def test_top_check_ignores_own_orders_updated() -> None: def test_bid_price_updated_at_instead_of_after() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(75), quantity=Decimal(7), side=mango.Side.BUY) + order: mango.Order = fake_order( + price=Decimal(75), quantity=Decimal(7), side=mango.Side.BUY + ) actual: TopOfBookElement = TopOfBookElement(Decimal(0)) result = actual.process(context, model_state, [order]) @@ -99,7 +121,9 @@ def test_bid_price_updated_at_instead_of_after() -> None: def test_ask_price_updated_at_instead_of_after() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(85), quantity=Decimal(6), side=mango.Side.SELL) + order: mango.Order = fake_order( + price=Decimal(85), quantity=Decimal(6), side=mango.Side.SELL + ) actual: TopOfBookElement = TopOfBookElement(Decimal(0)) result = actual.process(context, model_state, [order]) @@ -110,7 +134,9 @@ def test_ask_price_updated_at_instead_of_after() -> None: def test_bid_price_updated_two_ticks_better() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(75), quantity=Decimal(7), side=mango.Side.BUY) + order: mango.Order = fake_order( + price=Decimal(75), quantity=Decimal(7), side=mango.Side.BUY + ) actual: TopOfBookElement = TopOfBookElement(Decimal(2)) result = actual.process(context, model_state, [order]) @@ -121,7 +147,9 @@ def test_bid_price_updated_two_ticks_better() -> None: def test_ask_price_updated_two_ticks_better() -> None: context = fake_context() - order: mango.Order = fake_order(price=Decimal(85), quantity=Decimal(6), side=mango.Side.SELL) + order: mango.Order = fake_order( + price=Decimal(85), quantity=Decimal(6), side=mango.Side.SELL + ) actual: TopOfBookElement = TopOfBookElement(Decimal(2)) result = actual.process(context, model_state, [order]) diff --git a/tests/marketmaking/test_orderreconciler.py b/tests/marketmaking/test_orderreconciler.py index d95243e..e81b142 100644 --- a/tests/marketmaking/test_orderreconciler.py +++ b/tests/marketmaking/test_orderreconciler.py @@ -2,19 +2,30 @@ import mango from decimal import Decimal -from mango.marketmaking.orderreconciler import NullOrderReconciler, AlwaysReplaceOrderReconciler +from mango.marketmaking.orderreconciler import ( + NullOrderReconciler, + AlwaysReplaceOrderReconciler, +) from ..fakes import fake_model_state def test_nulloperation() -> None: existing = [ - mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10)), - mango.Order.from_basic_info(mango.Side.SELL, price=Decimal(2), quantity=Decimal(20)) + mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(1), quantity=Decimal(10) + ), + mango.Order.from_basic_info( + mango.Side.SELL, price=Decimal(2), quantity=Decimal(20) + ), ] desired = [ - mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(3), quantity=Decimal(30)), - mango.Order.from_basic_info(mango.Side.SELL, price=Decimal(4), quantity=Decimal(40)) + mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(3), quantity=Decimal(30) + ), + mango.Order.from_basic_info( + mango.Side.SELL, price=Decimal(4), quantity=Decimal(40) + ), ] model_state = fake_model_state() @@ -31,12 +42,20 @@ def test_nulloperation() -> None: def test_alwaysreplace() -> None: existing = [ - mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10)), - mango.Order.from_basic_info(mango.Side.SELL, price=Decimal(2), quantity=Decimal(20)) + mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(1), quantity=Decimal(10) + ), + mango.Order.from_basic_info( + mango.Side.SELL, price=Decimal(2), quantity=Decimal(20) + ), ] desired = [ - mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(3), quantity=Decimal(30)), - mango.Order.from_basic_info(mango.Side.SELL, price=Decimal(4), quantity=Decimal(40)) + mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(3), quantity=Decimal(30) + ), + mango.Order.from_basic_info( + mango.Side.SELL, price=Decimal(4), quantity=Decimal(40) + ), ] model_state = fake_model_state() diff --git a/tests/marketmaking/test_toleranceorderreconciler.py b/tests/marketmaking/test_toleranceorderreconciler.py index ec17655..1d92718 100644 --- a/tests/marketmaking/test_toleranceorderreconciler.py +++ b/tests/marketmaking/test_toleranceorderreconciler.py @@ -8,132 +8,200 @@ from ..fakes import fake_model_state def test_buy_does_not_match_sell() -> None: actual = ToleranceOrderReconciler(Decimal(1), Decimal(1)) - existing = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10)) - desired = mango.Order.from_basic_info(mango.Side.SELL, price=Decimal(1), quantity=Decimal(10)) + existing = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(1), quantity=Decimal(10) + ) + desired = mango.Order.from_basic_info( + mango.Side.SELL, price=Decimal(1), quantity=Decimal(10) + ) assert not actual.is_within_tolderance(existing, desired) def test_exact_match_with_small_tolerance_matches() -> None: actual = ToleranceOrderReconciler(Decimal("0.001"), Decimal("0.001")) - existing = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10)) - desired = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10)) + existing = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(1), quantity=Decimal(10) + ) + desired = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(1), quantity=Decimal(10) + ) assert actual.is_within_tolderance(existing, desired) def test_exact_match_with_zero_tolerance_matches() -> None: actual = ToleranceOrderReconciler(Decimal(0), Decimal(0)) - existing = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10)) - desired = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10)) + existing = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(1), quantity=Decimal(10) + ) + desired = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(1), quantity=Decimal(10) + ) assert actual.is_within_tolderance(existing, desired) def test_quantity_within_positive_tolerance_matches() -> None: actual = ToleranceOrderReconciler(Decimal(0), Decimal("0.001")) - existing = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10)) - desired = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal("10.009")) + existing = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(1), quantity=Decimal(10) + ) + desired = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(1), quantity=Decimal("10.009") + ) assert actual.is_within_tolderance(existing, desired) def test_quantity_positive_tolerance_boundary_matches() -> None: actual = ToleranceOrderReconciler(Decimal(0), Decimal("0.001")) - existing = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10)) - desired = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal("10.01")) + existing = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(1), quantity=Decimal(10) + ) + desired = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(1), quantity=Decimal("10.01") + ) assert actual.is_within_tolderance(existing, desired) def test_quantity_outside_positive_tolerance_no_match() -> None: actual = ToleranceOrderReconciler(Decimal(0), Decimal("0.001")) - existing = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10)) - desired = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal("10.011")) + existing = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(1), quantity=Decimal(10) + ) + desired = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(1), quantity=Decimal("10.011") + ) assert not actual.is_within_tolderance(existing, desired) def test_quantity_within_negative_tolerance_matches() -> None: actual = ToleranceOrderReconciler(Decimal(0), Decimal("0.001")) - existing = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10)) - desired = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal("9.991")) + existing = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(1), quantity=Decimal(10) + ) + desired = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(1), quantity=Decimal("9.991") + ) assert actual.is_within_tolderance(existing, desired) def test_quantity_negative_tolerance_boundary_matches() -> None: actual = ToleranceOrderReconciler(Decimal(0), Decimal("0.001")) - existing = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10)) - desired = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal("9.99")) + existing = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(1), quantity=Decimal(10) + ) + desired = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(1), quantity=Decimal("9.99") + ) assert actual.is_within_tolderance(existing, desired) def test_quantity_outside_negative_tolerance_no_match() -> None: actual = ToleranceOrderReconciler(Decimal(0), Decimal("0.001")) - existing = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10)) - desired = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal("9.989")) + existing = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(1), quantity=Decimal(10) + ) + desired = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(1), quantity=Decimal("9.989") + ) assert not actual.is_within_tolderance(existing, desired) def test_price_within_positive_tolerance_matches() -> None: actual = ToleranceOrderReconciler(Decimal("0.001"), Decimal(0)) - existing = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10)) - desired = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal("1.0009"), quantity=Decimal(10)) + existing = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(1), quantity=Decimal(10) + ) + desired = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal("1.0009"), quantity=Decimal(10) + ) assert actual.is_within_tolderance(existing, desired) def test_price_positive_tolerance_boundary_matches() -> None: actual = ToleranceOrderReconciler(Decimal("0.001"), Decimal(0)) - existing = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10)) - desired = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal("1.001"), quantity=Decimal(10)) + existing = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(1), quantity=Decimal(10) + ) + desired = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal("1.001"), quantity=Decimal(10) + ) assert actual.is_within_tolderance(existing, desired) def test_price_outside_positive_tolerance_no_match() -> None: actual = ToleranceOrderReconciler(Decimal("0.001"), Decimal(0)) - existing = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10)) - desired = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal("1.0011"), quantity=Decimal(10)) + existing = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(1), quantity=Decimal(10) + ) + desired = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal("1.0011"), quantity=Decimal(10) + ) assert not actual.is_within_tolderance(existing, desired) def test_price_within_negative_tolerance_matches() -> None: actual = ToleranceOrderReconciler(Decimal("0.001"), Decimal(0)) - existing = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10)) - desired = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal("0.9991"), quantity=Decimal(10)) + existing = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(1), quantity=Decimal(10) + ) + desired = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal("0.9991"), quantity=Decimal(10) + ) assert actual.is_within_tolderance(existing, desired) def test_price_negative_tolerance_boundary_matches() -> None: actual = ToleranceOrderReconciler(Decimal("0.001"), Decimal(0)) - existing = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10)) - desired = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal("0.999"), quantity=Decimal(10)) + existing = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(1), quantity=Decimal(10) + ) + desired = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal("0.999"), quantity=Decimal(10) + ) assert actual.is_within_tolderance(existing, desired) def test_price_outside_negative_tolerance_no_match() -> None: actual = ToleranceOrderReconciler(Decimal("0.001"), Decimal(0)) - existing = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(1), quantity=Decimal(10)) - desired = mango.Order.from_basic_info(mango.Side.BUY, price=Decimal("0.9989"), quantity=Decimal(10)) + existing = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(1), quantity=Decimal(10) + ) + desired = mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal("0.9989"), quantity=Decimal(10) + ) assert not actual.is_within_tolderance(existing, desired) def test_reconcile_no_acceptable_orders() -> None: existing = [ - mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(99), quantity=Decimal(10)), - mango.Order.from_basic_info(mango.Side.SELL, price=Decimal(101), quantity=Decimal(10)) + mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(99), quantity=Decimal(10) + ), + mango.Order.from_basic_info( + mango.Side.SELL, price=Decimal(101), quantity=Decimal(10) + ), ] desired = [ - mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(100), quantity=Decimal(10)), - mango.Order.from_basic_info(mango.Side.SELL, price=Decimal(102), quantity=Decimal(10)) + mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(100), quantity=Decimal(10) + ), + mango.Order.from_basic_info( + mango.Side.SELL, price=Decimal(102), quantity=Decimal(10) + ), ] model_state = fake_model_state() actual = ToleranceOrderReconciler(Decimal("0.001"), Decimal("0.001")) @@ -147,12 +215,20 @@ def test_reconcile_no_acceptable_orders() -> None: def test_reconcile_all_acceptable_orders() -> None: existing = [ - mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(99), quantity=Decimal(10)), - mango.Order.from_basic_info(mango.Side.SELL, price=Decimal(101), quantity=Decimal(10)) + mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(99), quantity=Decimal(10) + ), + mango.Order.from_basic_info( + mango.Side.SELL, price=Decimal(101), quantity=Decimal(10) + ), ] desired = [ - mango.Order.from_basic_info(mango.Side.BUY, price=Decimal("99.01"), quantity=Decimal(10)), - mango.Order.from_basic_info(mango.Side.SELL, price=Decimal("101.01"), quantity=Decimal(10)) + mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal("99.01"), quantity=Decimal(10) + ), + mango.Order.from_basic_info( + mango.Side.SELL, price=Decimal("101.01"), quantity=Decimal(10) + ), ] model_state = fake_model_state() actual = ToleranceOrderReconciler(Decimal("0.001"), Decimal("0.001")) @@ -166,14 +242,26 @@ def test_reconcile_all_acceptable_orders() -> None: def test_reconcile_different_list_sizes_orders() -> None: existing = [ - mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(98), quantity=Decimal(20)), - mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(99), quantity=Decimal(10)), - mango.Order.from_basic_info(mango.Side.SELL, price=Decimal(101), quantity=Decimal(10)), - mango.Order.from_basic_info(mango.Side.SELL, price=Decimal(102), quantity=Decimal(20)) + mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(98), quantity=Decimal(20) + ), + mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(99), quantity=Decimal(10) + ), + mango.Order.from_basic_info( + mango.Side.SELL, price=Decimal(101), quantity=Decimal(10) + ), + mango.Order.from_basic_info( + mango.Side.SELL, price=Decimal(102), quantity=Decimal(20) + ), ] desired = [ - mango.Order.from_basic_info(mango.Side.BUY, price=Decimal("99.01"), quantity=Decimal(10)), - mango.Order.from_basic_info(mango.Side.SELL, price=Decimal("101.01"), quantity=Decimal(10)) + mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal("99.01"), quantity=Decimal(10) + ), + mango.Order.from_basic_info( + mango.Side.SELL, price=Decimal("101.01"), quantity=Decimal(10) + ), ] model_state = fake_model_state() actual = ToleranceOrderReconciler(Decimal("0.001"), Decimal("0.001")) @@ -191,16 +279,32 @@ def test_reconcile_different_list_sizes_orders() -> None: def test_reconcile_two_acceptable_two_unacceptable_orders() -> None: existing = [ - mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(98), quantity=Decimal(20)), - mango.Order.from_basic_info(mango.Side.BUY, price=Decimal(99), quantity=Decimal(10)), - mango.Order.from_basic_info(mango.Side.SELL, price=Decimal(101), quantity=Decimal(10)), - mango.Order.from_basic_info(mango.Side.SELL, price=Decimal(102), quantity=Decimal(20)) + mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(98), quantity=Decimal(20) + ), + mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal(99), quantity=Decimal(10) + ), + mango.Order.from_basic_info( + mango.Side.SELL, price=Decimal(101), quantity=Decimal(10) + ), + mango.Order.from_basic_info( + mango.Side.SELL, price=Decimal(102), quantity=Decimal(20) + ), ] desired = [ - mango.Order.from_basic_info(mango.Side.BUY, price=Decimal("98.1"), quantity=Decimal(20)), - mango.Order.from_basic_info(mango.Side.BUY, price=Decimal("99.01"), quantity=Decimal(10)), - mango.Order.from_basic_info(mango.Side.SELL, price=Decimal("101.01"), quantity=Decimal(10)), - mango.Order.from_basic_info(mango.Side.SELL, price=Decimal("102.11"), quantity=Decimal(20)) + mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal("98.1"), quantity=Decimal(20) + ), + mango.Order.from_basic_info( + mango.Side.BUY, price=Decimal("99.01"), quantity=Decimal(10) + ), + mango.Order.from_basic_info( + mango.Side.SELL, price=Decimal("101.01"), quantity=Decimal(10) + ), + mango.Order.from_basic_info( + mango.Side.SELL, price=Decimal("102.11"), quantity=Decimal(20) + ), ] model_state = fake_model_state() actual = ToleranceOrderReconciler(Decimal("0.001"), Decimal("0.001")) diff --git a/tests/test_account.py b/tests/test_account.py index 77ee7cc..9a7f30b 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -2,7 +2,17 @@ import pytest from .context import mango from .data import load_data_from_directory -from .fakes import fake_account, fake_account_info, fake_context, fake_seeded_public_key, fake_token_bank, fake_instrument, fake_instrument_value, fake_perp_account, fake_token +from .fakes import ( + fake_account, + fake_account_info, + fake_context, + fake_seeded_public_key, + fake_token_bank, + fake_instrument, + fake_instrument_value, + fake_perp_account, + fake_token, +) from decimal import Decimal from mango.layouts import layouts @@ -21,8 +31,18 @@ def test_construction() -> None: quote_deposit = fake_instrument_value(raw_quote_deposit) raw_quote_borrow = Decimal(5) quote_borrow = fake_instrument_value(raw_quote_borrow) - quote = mango.AccountSlot(3, fake_instrument(), fake_token_bank(), fake_token_bank(), raw_quote_deposit, - quote_deposit, raw_quote_borrow, quote_borrow, None, None) + quote = mango.AccountSlot( + 3, + fake_instrument(), + fake_token_bank(), + fake_token_bank(), + raw_quote_deposit, + quote_deposit, + raw_quote_borrow, + quote_borrow, + None, + None, + ) raw_deposit1 = Decimal(1) deposit1 = fake_instrument_value(raw_deposit1) raw_deposit2 = Decimal(2) @@ -39,12 +59,42 @@ def test_construction() -> None: perp2 = fake_perp_account() perp3 = fake_perp_account() basket = [ - mango.AccountSlot(0, fake_instrument(), fake_token_bank(), fake_token_bank(), raw_deposit1, deposit1, - raw_borrow1, borrow1, fake_seeded_public_key("spot openorders 1"), perp1), - mango.AccountSlot(1, fake_instrument(), fake_token_bank(), fake_token_bank(), raw_deposit2, deposit2, - raw_borrow2, borrow2, fake_seeded_public_key("spot openorders 2"), perp2), - mango.AccountSlot(2, fake_instrument(), fake_token_bank(), fake_token_bank(), raw_deposit3, deposit3, - raw_borrow3, borrow3, fake_seeded_public_key("spot openorders 3"), perp3), + mango.AccountSlot( + 0, + fake_instrument(), + fake_token_bank(), + fake_token_bank(), + raw_deposit1, + deposit1, + raw_borrow1, + borrow1, + fake_seeded_public_key("spot openorders 1"), + perp1, + ), + mango.AccountSlot( + 1, + fake_instrument(), + fake_token_bank(), + fake_token_bank(), + raw_deposit2, + deposit2, + raw_borrow2, + borrow2, + fake_seeded_public_key("spot openorders 2"), + perp2, + ), + mango.AccountSlot( + 2, + fake_instrument(), + fake_token_bank(), + fake_token_bank(), + raw_deposit3, + deposit3, + raw_borrow3, + borrow3, + fake_seeded_public_key("spot openorders 3"), + perp3, + ), ] msrm_amount = Decimal(0) being_liquidated = False @@ -53,9 +103,25 @@ def test_construction() -> None: not_upgradable = False delegate = fake_seeded_public_key("delegate") - actual = mango.Account(account_info, mango.Version.V1, meta_data, "Test Group", group, owner, info, quote, - in_margin_basket, active_in_basket, basket, msrm_amount, being_liquidated, - is_bankrupt, advanced_orders, not_upgradable, delegate) + actual = mango.Account( + account_info, + mango.Version.V1, + meta_data, + "Test Group", + group, + owner, + info, + quote, + in_margin_basket, + active_in_basket, + basket, + msrm_amount, + being_liquidated, + is_bankrupt, + advanced_orders, + not_upgradable, + delegate, + ) assert actual is not None assert actual.version == mango.Version.V1 @@ -63,12 +129,38 @@ def test_construction() -> None: assert actual.owner == owner assert actual.slot_indices == active_in_basket assert actual.in_margin_basket == in_margin_basket - assert actual.deposits_by_index == [None, deposit1, None, deposit2, deposit3, quote_deposit] - assert actual.borrows_by_index == [None, borrow1, None, borrow2, borrow3, quote_borrow] - assert actual.net_values_by_index == [None, deposit1 - borrow1, None, - deposit2 - borrow2, deposit3 - borrow3, quote_deposit - quote_borrow] - assert actual.spot_open_orders_by_index == [None, fake_seeded_public_key("spot openorders 1"), None, fake_seeded_public_key( - "spot openorders 2"), fake_seeded_public_key("spot openorders 3"), None] + assert actual.deposits_by_index == [ + None, + deposit1, + None, + deposit2, + deposit3, + quote_deposit, + ] + assert actual.borrows_by_index == [ + None, + borrow1, + None, + borrow2, + borrow3, + quote_borrow, + ] + assert actual.net_values_by_index == [ + None, + deposit1 - borrow1, + None, + deposit2 - borrow2, + deposit3 - borrow3, + quote_deposit - quote_borrow, + ] + assert actual.spot_open_orders_by_index == [ + None, + fake_seeded_public_key("spot openorders 1"), + None, + fake_seeded_public_key("spot openorders 2"), + fake_seeded_public_key("spot openorders 3"), + None, + ] assert actual.perp_accounts_by_index == [None, perp1, None, perp2, perp3, None] assert actual.msrm_amount == msrm_amount assert actual.being_liquidated == being_liquidated @@ -87,22 +179,57 @@ def test_slot_lookups() -> None: in_margin_basket = [False, False, False, False, False, False, False] active_in_basket = [False, True, False, True, True, False, False] zero_value = Decimal(0) - quote_slot = mango.AccountSlot(3, fake_token("FAKEQUOTE"), fake_token_bank("FAKEQUOTE"), fake_token_bank(), zero_value, - fake_instrument_value(Decimal(10)), zero_value, fake_instrument_value(Decimal(2)), None, None) + quote_slot = mango.AccountSlot( + 3, + fake_token("FAKEQUOTE"), + fake_token_bank("FAKEQUOTE"), + fake_token_bank(), + zero_value, + fake_instrument_value(Decimal(10)), + zero_value, + fake_instrument_value(Decimal(2)), + None, + None, + ) perp2 = fake_perp_account() perp3 = fake_perp_account() slots = [ - mango.AccountSlot(1, fake_instrument("slot1"), fake_token_bank(), fake_token_bank(), - zero_value, fake_instrument_value(Decimal(2)), - zero_value, fake_instrument_value(Decimal(1)), - fake_seeded_public_key("spot openorders 1"), None), - mango.AccountSlot(3, fake_token("MNGO"), fake_token_bank("MNGO"), fake_token_bank(), - zero_value, fake_instrument_value(Decimal(6)), - zero_value, fake_instrument_value(Decimal(4)), - fake_seeded_public_key("spot openorders 2"), perp2), - mango.AccountSlot(4, fake_instrument("slot3"), fake_token_bank(), fake_token_bank(), - zero_value, fake_instrument_value(Decimal(5)), - zero_value, fake_instrument_value(Decimal(8)), None, perp3), + mango.AccountSlot( + 1, + fake_instrument("slot1"), + fake_token_bank(), + fake_token_bank(), + zero_value, + fake_instrument_value(Decimal(2)), + zero_value, + fake_instrument_value(Decimal(1)), + fake_seeded_public_key("spot openorders 1"), + None, + ), + mango.AccountSlot( + 3, + fake_token("MNGO"), + fake_token_bank("MNGO"), + fake_token_bank(), + zero_value, + fake_instrument_value(Decimal(6)), + zero_value, + fake_instrument_value(Decimal(4)), + fake_seeded_public_key("spot openorders 2"), + perp2, + ), + mango.AccountSlot( + 4, + fake_instrument("slot3"), + fake_token_bank(), + fake_token_bank(), + zero_value, + fake_instrument_value(Decimal(5)), + zero_value, + fake_instrument_value(Decimal(8)), + None, + perp3, + ), ] msrm_amount = Decimal(0) being_liquidated = False @@ -111,9 +238,25 @@ def test_slot_lookups() -> None: not_upgradable = False delegate = fake_seeded_public_key("delegate") - actual = mango.Account(account_info, mango.Version.V1, meta_data, "Test Group", group, owner, info, quote_slot, - in_margin_basket, active_in_basket, slots, msrm_amount, being_liquidated, - is_bankrupt, advanced_orders, not_upgradable, delegate) + actual = mango.Account( + account_info, + mango.Version.V1, + meta_data, + "Test Group", + group, + owner, + info, + quote_slot, + in_margin_basket, + active_in_basket, + slots, + msrm_amount, + being_liquidated, + is_bankrupt, + advanced_orders, + not_upgradable, + delegate, + ) assert actual.shared_quote == quote_slot assert actual.shared_quote_token == quote_slot.base_instrument @@ -192,7 +335,9 @@ def test_slot_lookups() -> None: assert actual.spot_open_orders[0] == slots[0].spot_open_orders assert actual.spot_open_orders[1] == slots[1].spot_open_orders - assert len(actual.spot_open_orders_by_index) == 8 # Shared Quote is included but should always be None + assert ( + len(actual.spot_open_orders_by_index) == 8 + ) # Shared Quote is included but should always be None assert actual.spot_open_orders_by_index[0] is None assert actual.spot_open_orders_by_index[1] == slots[0].spot_open_orders assert actual.spot_open_orders_by_index[2] is None @@ -207,7 +352,9 @@ def test_slot_lookups() -> None: assert actual.perp_accounts[0] == slots[1].perp_account assert actual.perp_accounts[1] == slots[2].perp_account - assert len(actual.perp_accounts_by_index) == 8 # Shared Quote is included but should always be None + assert ( + len(actual.perp_accounts_by_index) == 8 + ) # Shared Quote is included but should always be None assert actual.perp_accounts_by_index[0] is None assert actual.perp_accounts_by_index[1] is None assert actual.perp_accounts_by_index[2] is None @@ -228,7 +375,9 @@ def test_slot_lookups() -> None: def test_loaded_account_slot_lookups() -> None: - group, cache, account, open_orders = load_data_from_directory("tests/testdata/account5") + group, cache, account, open_orders = load_data_from_directory( + "tests/testdata/account5" + ) assert len(account.slots) == 14 # ยซ GroupSlot[0] ยซ Token [MNGO] 'MNGO' [Bb9bsTQa1bGEtQ5KagGkvSHyuLqDWumFUcRqFusFNJWC (6 decimals)] ยป @@ -289,8 +438,12 @@ def test_loaded_account_slot_lookups() -> None: def test_derive_referrer_memory_address() -> None: - context = fake_context(mango_program_address=PublicKey("4skJ85cdxQAFVKbcGgfun8iZPL7BadVYXG3kGEGkufqA")) - account = fake_account(address=PublicKey("FG99s25HS1UKcP1jMx72Gezg6KZCC7DuKXhNW51XC1qi")) + context = fake_context( + mango_program_address=PublicKey("4skJ85cdxQAFVKbcGgfun8iZPL7BadVYXG3kGEGkufqA") + ) + account = fake_account( + address=PublicKey("FG99s25HS1UKcP1jMx72Gezg6KZCC7DuKXhNW51XC1qi") + ) actual = account.derive_referrer_memory_address(context) # Value derived using mango-client-v3: 3CMpC1UzdLrAnGz6HZVoBsDLAHpTABkUJr8iPyEHwehr diff --git a/tests/test_accountflags.py b/tests/test_accountflags.py index 9066918..ae7af72 100644 --- a/tests/test_accountflags.py +++ b/tests/test_accountflags.py @@ -10,8 +10,17 @@ def test_constructor() -> None: bids: bool = True asks: bool = True disabled: bool = True - actual = mango.AccountFlags(mango.Version.V1, initialized, market, open_orders, - request_queue, event_queue, bids, asks, disabled) + actual = mango.AccountFlags( + mango.Version.V1, + initialized, + market, + open_orders, + request_queue, + event_queue, + bids, + asks, + disabled, + ) assert actual is not None assert actual.version == mango.Version.V1 assert actual.initialized == initialized @@ -23,9 +32,17 @@ def test_constructor() -> None: assert actual.asks == asks assert actual.disabled == disabled - actual2 = mango.AccountFlags(mango.Version.V2, not initialized, not market, - not open_orders, not request_queue, not event_queue, - not bids, not asks, not disabled) + actual2 = mango.AccountFlags( + mango.Version.V2, + not initialized, + not market, + not open_orders, + not request_queue, + not event_queue, + not bids, + not asks, + not disabled, + ) assert actual2 is not None assert actual2.version == mango.Version.V2 assert actual2.initialized == (not initialized) diff --git a/tests/test_cache.py b/tests/test_cache.py index 5afe2fd..5b8309a 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -10,12 +10,25 @@ from decimal import Decimal def test_cache_constructor() -> None: account_info = fake_account_info(fake_seeded_public_key("cache")) - meta_data = mango.Metadata(mango.layouts.DATA_TYPE.parse(bytearray(b'\x07')), mango.Version.V1, True) + meta_data = mango.Metadata( + mango.layouts.DATA_TYPE.parse(bytearray(b"\x07")), mango.Version.V1, True + ) timestamp = datetime.now() price_cache = [mango.PriceCache(Decimal(26), timestamp)] - root_bank_cache = [mango.RootBankCache(Decimal("0.00001"), Decimal("0.00001"), timestamp)] - perp_market_cache = [mango.PerpMarketCache(Decimal("0.00002"), Decimal("0.00002"), timestamp)] - actual = mango.Cache(account_info, mango.Version.V1, meta_data, price_cache, root_bank_cache, perp_market_cache) + root_bank_cache = [ + mango.RootBankCache(Decimal("0.00001"), Decimal("0.00001"), timestamp) + ] + perp_market_cache = [ + mango.PerpMarketCache(Decimal("0.00002"), Decimal("0.00002"), timestamp) + ] + actual = mango.Cache( + account_info, + mango.Version.V1, + meta_data, + price_cache, + root_bank_cache, + perp_market_cache, + ) assert actual is not None assert actual.account_info == account_info @@ -35,13 +48,25 @@ def test_load_cache() -> None: # actual_pc: typing.Sequence[typing.Optional[mango.PriceCache]] = cache.price_cache - assert actual_pc[0] is not None and actual_pc[0].price == Decimal("0.33642499999999841975") - assert actual_pc[1] is not None and actual_pc[1].price == Decimal("47380.32499999999999928946") - assert actual_pc[2] is not None and actual_pc[2].price == Decimal("3309.69549999999999911893") - assert actual_pc[3] is not None and actual_pc[3].price == Decimal("0.17261599999999788224") - assert actual_pc[4] is not None and actual_pc[4].price == Decimal("8.79379999999999739657") + assert actual_pc[0] is not None and actual_pc[0].price == Decimal( + "0.33642499999999841975" + ) + assert actual_pc[1] is not None and actual_pc[1].price == Decimal( + "47380.32499999999999928946" + ) + assert actual_pc[2] is not None and actual_pc[2].price == Decimal( + "3309.69549999999999911893" + ) + assert actual_pc[3] is not None and actual_pc[3].price == Decimal( + "0.17261599999999788224" + ) + assert actual_pc[4] is not None and actual_pc[4].price == Decimal( + "8.79379999999999739657" + ) assert actual_pc[5] is not None and actual_pc[5].price == Decimal("1") - assert actual_pc[6] is not None and actual_pc[6].price == Decimal("1.00039999999999906777") + assert actual_pc[6] is not None and actual_pc[6].price == Decimal( + "1.00039999999999906777" + ) assert actual_pc[7] is None assert actual_pc[8] is None assert actual_pc[9] is None @@ -51,21 +76,51 @@ def test_load_cache() -> None: assert actual_pc[13] is None assert actual_pc[14] is None - actual_rbc: typing.Sequence[typing.Optional[mango.RootBankCache]] = cache.root_bank_cache - assert actual_rbc[0] is not None and actual_rbc[0].deposit_index == Decimal("1001923.86460821722014813417") - assert actual_rbc[0] is not None and actual_rbc[0].borrow_index == Decimal("1002515.45257855337824182129") - assert actual_rbc[1] is not None and actual_rbc[1].deposit_index == Decimal("1000007.37249653914441083202") - assert actual_rbc[1] is not None and actual_rbc[1].borrow_index == Decimal("1000166.98522159213999316307") - assert actual_rbc[2] is not None and actual_rbc[2].deposit_index == Decimal("1000000.19554886875829424753") - assert actual_rbc[2] is not None and actual_rbc[2].borrow_index == Decimal("1000001.13273253565107623331") - assert actual_rbc[3] is not None and actual_rbc[3].deposit_index == Decimal("1000037.82149923799070379005") - assert actual_rbc[3] is not None and actual_rbc[3].borrow_index == Decimal("1000044.28925241010965052624") - assert actual_rbc[4] is not None and actual_rbc[4].deposit_index == Decimal("1000000.0000132182767842437") - assert actual_rbc[4] is not None and actual_rbc[4].borrow_index == Decimal("1000000.14235973938041368569") - assert actual_rbc[5] is not None and actual_rbc[5].deposit_index == Decimal("1000000.35244386506945346582") - assert actual_rbc[5] is not None and actual_rbc[5].borrow_index == Decimal("1000000.66156146420993522383") - assert actual_rbc[6] is not None and actual_rbc[6].deposit_index == Decimal("1000473.25161608998580575758") - assert actual_rbc[6] is not None and actual_rbc[6].borrow_index == Decimal("1000524.37279217702128875089") + actual_rbc: typing.Sequence[ + typing.Optional[mango.RootBankCache] + ] = cache.root_bank_cache + assert actual_rbc[0] is not None and actual_rbc[0].deposit_index == Decimal( + "1001923.86460821722014813417" + ) + assert actual_rbc[0] is not None and actual_rbc[0].borrow_index == Decimal( + "1002515.45257855337824182129" + ) + assert actual_rbc[1] is not None and actual_rbc[1].deposit_index == Decimal( + "1000007.37249653914441083202" + ) + assert actual_rbc[1] is not None and actual_rbc[1].borrow_index == Decimal( + "1000166.98522159213999316307" + ) + assert actual_rbc[2] is not None and actual_rbc[2].deposit_index == Decimal( + "1000000.19554886875829424753" + ) + assert actual_rbc[2] is not None and actual_rbc[2].borrow_index == Decimal( + "1000001.13273253565107623331" + ) + assert actual_rbc[3] is not None and actual_rbc[3].deposit_index == Decimal( + "1000037.82149923799070379005" + ) + assert actual_rbc[3] is not None and actual_rbc[3].borrow_index == Decimal( + "1000044.28925241010965052624" + ) + assert actual_rbc[4] is not None and actual_rbc[4].deposit_index == Decimal( + "1000000.0000132182767842437" + ) + assert actual_rbc[4] is not None and actual_rbc[4].borrow_index == Decimal( + "1000000.14235973938041368569" + ) + assert actual_rbc[5] is not None and actual_rbc[5].deposit_index == Decimal( + "1000000.35244386506945346582" + ) + assert actual_rbc[5] is not None and actual_rbc[5].borrow_index == Decimal( + "1000000.66156146420993522383" + ) + assert actual_rbc[6] is not None and actual_rbc[6].deposit_index == Decimal( + "1000473.25161608998580575758" + ) + assert actual_rbc[6] is not None and actual_rbc[6].borrow_index == Decimal( + "1000524.37279217702128875089" + ) assert actual_rbc[7] is None assert actual_rbc[7] is None assert actual_rbc[8] is None @@ -82,17 +137,31 @@ def test_load_cache() -> None: assert actual_rbc[13] is None assert actual_rbc[14] is None assert actual_rbc[14] is None - assert actual_rbc[15] is not None and actual_rbc[15].deposit_index == Decimal("1000154.42276607534055088422") - assert actual_rbc[15] is not None and actual_rbc[15].borrow_index == Decimal("1000219.00868743509063563124") + assert actual_rbc[15] is not None and actual_rbc[15].deposit_index == Decimal( + "1000154.42276607534055088422" + ) + assert actual_rbc[15] is not None and actual_rbc[15].borrow_index == Decimal( + "1000219.00868743509063563124" + ) - actual_pmc: typing.Sequence[typing.Optional[mango.PerpMarketCache]] = cache.perp_market_cache + actual_pmc: typing.Sequence[ + typing.Optional[mango.PerpMarketCache] + ] = cache.perp_market_cache assert actual_pmc[0] is None - assert actual_pmc[1] is not None and actual_pmc[1].long_funding == Decimal("-751864.70031280454435673732") - assert actual_pmc[1] is not None and actual_pmc[1].short_funding == Decimal("-752275.3557979761382519257") + assert actual_pmc[1] is not None and actual_pmc[1].long_funding == Decimal( + "-751864.70031280454435673732" + ) + assert actual_pmc[1] is not None and actual_pmc[1].short_funding == Decimal( + "-752275.3557979761382519257" + ) assert actual_pmc[2] is not None and actual_pmc[2].long_funding == Decimal("0") assert actual_pmc[2] is not None and actual_pmc[2].short_funding == Decimal("0") - assert actual_pmc[3] is not None and actual_pmc[3].long_funding == Decimal("-636425.51790158202868497028") - assert actual_pmc[3] is not None and actual_pmc[3].short_funding == Decimal("-636425.51790158202868497028") + assert actual_pmc[3] is not None and actual_pmc[3].long_funding == Decimal( + "-636425.51790158202868497028" + ) + assert actual_pmc[3] is not None and actual_pmc[3].short_funding == Decimal( + "-636425.51790158202868497028" + ) assert actual_pmc[4] is None assert actual_pmc[5] is None assert actual_pmc[6] is None diff --git a/tests/test_client.py b/tests/test_client.py index a8eab7b..89a566c 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -11,26 +11,40 @@ __FAKE_RPC_METHOD = RPCMethod("fake") class FakeRPCCaller(mango.RPCCaller): def __init__(self) -> None: - super().__init__("Fake", "https://localhost", "wss://localhost", -1, [0.1, 0.2], mango.SlotHolder(), mango.InstructionReporter()) + super().__init__( + "Fake", + "https://localhost", + "wss://localhost", + -1, + [0.1, 0.2], + mango.SlotHolder(), + mango.InstructionReporter(), + ) self.called = False def make_request(self, method: RPCMethod, *params: typing.Any) -> RPCResponse: self.called = True - return { - "jsonrpc": "2.0", - "id": 0, - "result": {} - } + return {"jsonrpc": "2.0", "id": 0, "result": {}} class RaisingRPCCaller(mango.RPCCaller): def __init__(self) -> None: - super().__init__("Fake", "https://localhost", "wss://localhost", -1, [0.1, 0.2], mango.SlotHolder(), mango.InstructionReporter()) + super().__init__( + "Fake", + "https://localhost", + "wss://localhost", + -1, + [0.1, 0.2], + mango.SlotHolder(), + mango.InstructionReporter(), + ) self.called = False def make_request(self, method: RPCMethod, *params: typing.Any) -> RPCResponse: self.called = True - raise mango.TooManyRequestsRateLimitException("Fake", "fake-name", "https://fake") + raise mango.TooManyRequestsRateLimitException( + "Fake", "fake-name", "https://fake" + ) def test_constructor_sets_correct_values() -> None: diff --git a/tests/test_context.py b/tests/test_context.py index 35a5b7d..051b820 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -5,10 +5,16 @@ from solana.publickey import PublicKey def context_has_default_values(ctx: mango.Context) -> None: - assert ctx.mango_program_address == PublicKey("mv3ekLzLbnVPNxjSKvqBpU3ZeZXPQdEC3bp5MDEBG68") - assert ctx.serum_program_address == PublicKey("9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin") + assert ctx.mango_program_address == PublicKey( + "mv3ekLzLbnVPNxjSKvqBpU3ZeZXPQdEC3bp5MDEBG68" + ) + assert ctx.serum_program_address == PublicKey( + "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin" + ) assert ctx.group_name == "mainnet.1" - assert ctx.group_address == PublicKey("98pjRuQjK3qA6gXts96PqZT4Ze5QmnCmt3QYjhbUSPue") + assert ctx.group_address == PublicKey( + "98pjRuQjK3qA6gXts96PqZT4Ze5QmnCmt3QYjhbUSPue" + ) assert ctx.gma_chunk_size == Decimal(100) assert ctx.gma_chunk_pause == Decimal(0) @@ -27,8 +33,14 @@ def test_new_from_cluster() -> None: assert derived.client.cluster_name == "devnet" assert derived.client.cluster_rpc_url == "https://mango.devnet.rpcpool.com" assert derived.client.cluster_ws_url == "wss://mango.devnet.rpcpool.com" - assert derived.mango_program_address == PublicKey("4skJ85cdxQAFVKbcGgfun8iZPL7BadVYXG3kGEGkufqA") - assert derived.serum_program_address == PublicKey("DESVgJVGajEgKGXhb6XmqDHGz3VjdgP7rEVESBgxmroY") + assert derived.mango_program_address == PublicKey( + "4skJ85cdxQAFVKbcGgfun8iZPL7BadVYXG3kGEGkufqA" + ) + assert derived.serum_program_address == PublicKey( + "DESVgJVGajEgKGXhb6XmqDHGz3VjdgP7rEVESBgxmroY" + ) assert derived.group_name == "devnet.2" - assert derived.group_address == PublicKey("Ec2enZyoC4nGpEfu2sUNAa2nUGJHWxoUWYSEJ2hNTWTA") + assert derived.group_address == PublicKey( + "Ec2enZyoC4nGpEfu2sUNAa2nUGJHWxoUWYSEJ2hNTWTA" + ) context_has_default_values(mango.ContextBuilder.default()) diff --git a/tests/test_group.py b/tests/test_group.py index 8a728d1..b3f94d3 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -3,7 +3,14 @@ import typing from .context import mango from .data import load_group -from .fakes import fake_account_info, fake_context, fake_group, fake_seeded_public_key, fake_token_bank, fake_instrument +from .fakes import ( + fake_account_info, + fake_context, + fake_group, + fake_seeded_public_key, + fake_token_bank, + fake_instrument, +) from decimal import Decimal from mango.layouts import layouts @@ -33,11 +40,30 @@ def test_construction() -> None: referral_share_centibps = Decimal(8) referral_mngo_required = Decimal(9) - actual = mango.Group(account_info, mango.Version.V1, name, meta_data, shared_quote_token, in_basket, - slots, signer_nonce, signer_key, admin_key, serum_program_address, - cache_key, 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) + actual = mango.Group( + account_info, + mango.Version.V1, + name, + meta_data, + shared_quote_token, + in_basket, + slots, + signer_nonce, + signer_key, + admin_key, + serum_program_address, + cache_key, + 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, + ) assert actual is not None assert actual.name == name @@ -86,35 +112,115 @@ def test_slot_lookups() -> None: slot1_token_bank = fake_token_bank("slot1") mngo_token_bank = fake_token_bank("MNGO") slot3_instrument = fake_instrument("slot3") - in_basket: typing.Sequence[bool] = [False, True, False, False, True, False, True, False] - spot_market1 = mango.GroupSlotSpotMarket(fake_seeded_public_key("spot market 1"), - Decimal(0), Decimal(0), Decimal(0), Decimal(0)) - spot_market2 = mango.GroupSlotSpotMarket(fake_seeded_public_key("spot market 2"), - Decimal(0), Decimal(0), Decimal(0), Decimal(0)) - perp_market2 = mango.GroupSlotPerpMarket(fake_seeded_public_key("perp market 2"), Decimal(0), Decimal(0), - Decimal(0), Decimal(0), Decimal(0), Decimal(0), Decimal(0)) - perp_market3 = mango.GroupSlotPerpMarket(fake_seeded_public_key("perp market 3"), Decimal(0), Decimal(0), - Decimal(0), Decimal(0), Decimal(0), Decimal(0), Decimal(0)) - slot1 = mango.GroupSlot(1, slot1_token_bank.token, slot1_token_bank, shared_quote_token_bank, spot_market1, - None, mango.NullLotSizeConverter(), fake_seeded_public_key("oracle 1")) + in_basket: typing.Sequence[bool] = [ + False, + True, + False, + False, + True, + False, + True, + False, + ] + spot_market1 = mango.GroupSlotSpotMarket( + fake_seeded_public_key("spot market 1"), + Decimal(0), + Decimal(0), + Decimal(0), + Decimal(0), + ) + spot_market2 = mango.GroupSlotSpotMarket( + fake_seeded_public_key("spot market 2"), + Decimal(0), + Decimal(0), + Decimal(0), + Decimal(0), + ) + perp_market2 = mango.GroupSlotPerpMarket( + fake_seeded_public_key("perp market 2"), + Decimal(0), + Decimal(0), + Decimal(0), + Decimal(0), + Decimal(0), + Decimal(0), + Decimal(0), + ) + perp_market3 = mango.GroupSlotPerpMarket( + fake_seeded_public_key("perp market 3"), + Decimal(0), + Decimal(0), + Decimal(0), + Decimal(0), + Decimal(0), + Decimal(0), + Decimal(0), + ) + slot1 = mango.GroupSlot( + 1, + slot1_token_bank.token, + slot1_token_bank, + shared_quote_token_bank, + spot_market1, + None, + mango.NullLotSizeConverter(), + fake_seeded_public_key("oracle 1"), + ) # MNGO is a special case since that's the current name used for liquidity tokens. - slot2 = mango.GroupSlot(4, mngo_token_bank.token, mngo_token_bank, shared_quote_token_bank, spot_market2, - perp_market2, mango.NullLotSizeConverter(), fake_seeded_public_key("oracle 2")) - slot3 = mango.GroupSlot(6, slot3_instrument, None, shared_quote_token_bank, None, perp_market3, - mango.NullLotSizeConverter(), fake_seeded_public_key("oracle 3")) + slot2 = mango.GroupSlot( + 4, + mngo_token_bank.token, + mngo_token_bank, + shared_quote_token_bank, + spot_market2, + perp_market2, + mango.NullLotSizeConverter(), + fake_seeded_public_key("oracle 2"), + ) + slot3 = mango.GroupSlot( + 6, + slot3_instrument, + None, + shared_quote_token_bank, + None, + perp_market3, + mango.NullLotSizeConverter(), + fake_seeded_public_key("oracle 3"), + ) slots: typing.Sequence[mango.GroupSlot] = [slot1, slot2, slot3] - actual = mango.Group(account_info, mango.Version.V1, name, meta_data, shared_quote_token_bank, in_basket, - slots, signer_nonce, signer_key, admin_key, serum_program_address, - cache_key, 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) + actual = mango.Group( + account_info, + mango.Version.V1, + name, + meta_data, + shared_quote_token_bank, + in_basket, + slots, + signer_nonce, + signer_key, + admin_key, + serum_program_address, + cache_key, + 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, + ) assert actual.shared_quote == shared_quote_token_bank assert actual.liquidity_incentive_token_bank == mngo_token_bank assert actual.liquidity_incentive_token == mngo_token_bank.token - assert len(actual.tokens) == 3 # Shared Quote is included, slot3 has no TokenBank so is not a Token + assert ( + len(actual.tokens) == 3 + ) # Shared Quote is included, slot3 has no TokenBank so is not a Token assert actual.tokens[0] == slot1.base_token_bank assert actual.tokens[1] == slot2.base_token_bank assert actual.tokens[2] == shared_quote_token_bank @@ -202,11 +308,23 @@ def test_slot_lookups() -> None: assert actual.perp_markets_by_index[6] == slot3.perp_market assert actual.perp_markets_by_index[7] is None - assert actual.slot_by_spot_market_address(fake_seeded_public_key("spot market 1")) == slot1 - assert actual.slot_by_spot_market_address(fake_seeded_public_key("spot market 2")) == slot2 + assert ( + actual.slot_by_spot_market_address(fake_seeded_public_key("spot market 1")) + == slot1 + ) + assert ( + actual.slot_by_spot_market_address(fake_seeded_public_key("spot market 2")) + == slot2 + ) - assert actual.slot_by_perp_market_address(fake_seeded_public_key("perp market 2")) == slot2 - assert actual.slot_by_perp_market_address(fake_seeded_public_key("perp market 3")) == slot3 + assert ( + actual.slot_by_perp_market_address(fake_seeded_public_key("perp market 2")) + == slot2 + ) + assert ( + actual.slot_by_perp_market_address(fake_seeded_public_key("perp market 3")) + == slot3 + ) assert actual.slot_by_instrument_or_none(fake_instrument()) is None assert actual.slot_by_instrument_or_none(fake_instrument("slot3")) == slot3 @@ -275,8 +393,12 @@ def test_loaded_group_slot_lookups() -> None: def test_derive_referrer_record_address() -> None: - context = fake_context(mango_program_address=PublicKey("4skJ85cdxQAFVKbcGgfun8iZPL7BadVYXG3kGEGkufqA")) - group = fake_group(address=PublicKey("Ec2enZyoC4nGpEfu2sUNAa2nUGJHWxoUWYSEJ2hNTWTA")) + context = fake_context( + mango_program_address=PublicKey("4skJ85cdxQAFVKbcGgfun8iZPL7BadVYXG3kGEGkufqA") + ) + group = fake_group( + address=PublicKey("Ec2enZyoC4nGpEfu2sUNAa2nUGJHWxoUWYSEJ2hNTWTA") + ) actual = group.derive_referrer_record_address(context, "Test") # Value derived using mango-client-v3: 2rZyTeG2K45oLWiGHBZKdcsWig5PL5c3yUa9Fc35mY48 diff --git a/tests/test_healthcalculator.py b/tests/test_healthcalculator.py index 4742f7d..0079e60 100644 --- a/tests/test_healthcalculator.py +++ b/tests/test_healthcalculator.py @@ -4,7 +4,9 @@ from decimal import Decimal def test_empty() -> None: - group, cache, account, open_orders = load_data_from_directory("tests/testdata/empty") + group, cache, account, open_orders = load_data_from_directory( + "tests/testdata/empty" + ) frame = account.to_dataframe(group, open_orders, cache) # Typescript says: 0 @@ -29,14 +31,20 @@ def test_empty() -> None: def test_1deposit() -> None: - group, cache, account, open_orders = load_data_from_directory("tests/testdata/1deposit") + group, cache, account, open_orders = load_data_from_directory( + "tests/testdata/1deposit" + ) frame = account.to_dataframe(group, open_orders, cache) # Typescript says: 37904260000.05905822642118252475 - assert account.init_health(frame) == Decimal("37904.2600000591928892771752953600134") + assert account.init_health(frame) == Decimal( + "37904.2600000591928892771752953600134" + ) # Typescript says: 42642292500.06652466908819931746 - assert account.maint_health(frame) == Decimal("42642.2925000665920004368222072800150") + assert account.maint_health(frame) == Decimal( + "42642.2925000665920004368222072800150" + ) # Typescript says: 100 assert account.init_health_ratio(frame) == Decimal("100") @@ -45,7 +53,9 @@ def test_1deposit() -> None: assert account.maint_health_ratio(frame) == Decimal("100") # Typescript says: 47380.32499999999999928946 - assert account.total_value(frame) == Decimal("47380.3250000739911115964691192000167") + assert account.total_value(frame) == Decimal( + "47380.3250000739911115964691192000167" + ) # Typescript says: 0 assert account.leverage(frame) == Decimal("0") @@ -54,23 +64,33 @@ def test_1deposit() -> None: def test_account1() -> None: - group, cache, account, open_orders = load_data_from_directory("tests/testdata/account1") + group, cache, account, open_orders = load_data_from_directory( + "tests/testdata/account1" + ) frame = account.to_dataframe(group, open_orders, cache) # Typescript says: 454884281.15520619643754685058 assert account.init_health(frame) == Decimal("454.88428115521887496581258174978975") # Typescript says: 901472688.63722587052636470162 - assert account.maint_health(frame) == Decimal("901.47268863723220971375597908220329") + assert account.maint_health(frame) == Decimal( + "901.47268863723220971375597908220329" + ) # Typescript says: 10.48860467608925262084 - assert account.init_health_ratio(frame) == Decimal("10.4886046760897514671034971997305770") + assert account.init_health_ratio(frame) == Decimal( + "10.4886046760897514671034971997305770" + ) # Typescript says: 20.785925232226531989 - assert account.maint_health_ratio(frame) == Decimal("20.7859252322269328739250898812969450") + assert account.maint_health_ratio(frame) == Decimal( + "20.7859252322269328739250898812969450" + ) # Typescript says: 1348.25066158888197520582 - assert account.total_value(frame) == Decimal("1348.25066711924554446169937641461683") + assert account.total_value(frame) == Decimal( + "1348.25066711924554446169937641461683" + ) # Typescript says: 3.21671490144456129201 assert account.leverage(frame) == Decimal("3.21671488765457268128834463982425497") @@ -79,23 +99,33 @@ def test_account1() -> None: def test_account2() -> None: - group, cache, account, open_orders = load_data_from_directory("tests/testdata/account2") + group, cache, account, open_orders = load_data_from_directory( + "tests/testdata/account2" + ) frame = account.to_dataframe(group, open_orders, cache) # Typescript says: 7516159604.84918334545095675026 assert account.init_health(frame) == Decimal("7516.1596048492430563556697582309637") # Typescript says: 9618709877.45119083596852505025 - assert account.maint_health(frame) == Decimal("9618.7098774512206992595893853316522") + assert account.maint_health(frame) == Decimal( + "9618.7098774512206992595893853316522" + ) # Typescript says: 24.80680004365716229131 - assert account.init_health_ratio(frame) == Decimal("24.8068000436574936267384623925241840") + assert account.init_health_ratio(frame) == Decimal( + "24.8068000436574936267384623925241840" + ) # Typescript says: 31.74618756817508824497 - assert account.maint_health_ratio(frame) == Decimal("31.7461875681752057505441268626950890") + assert account.maint_health_ratio(frame) == Decimal( + "31.7461875681752057505441268626950890" + ) # Typescript says: 11721.35669142618275273549 - assert account.total_value(frame) == Decimal("11721.3566920531983421635090124323407") + assert account.total_value(frame) == Decimal( + "11721.3566920531983421635090124323407" + ) # Typescript says: 3.56338611204225585993 assert account.leverage(frame) == Decimal("3.56338611185164025806595342485346312") @@ -104,23 +134,33 @@ def test_account2() -> None: def test_account3() -> None: - group, cache, account, open_orders = load_data_from_directory("tests/testdata/account3") + group, cache, account, open_orders = load_data_from_directory( + "tests/testdata/account3" + ) frame = account.to_dataframe(group, open_orders, cache) # Typescript says: 341025333625.51856223547208912805 assert account.init_health(frame) == Decimal("341025.33362550396263557255539801613") # Typescript says: 683477170424.20340250929429970483 - assert account.maint_health(frame) == Decimal("683477.17042418393637609525383421613") + assert account.maint_health(frame) == Decimal( + "683477.17042418393637609525383421613" + ) # Typescript says: 4.52652018845647319267 - assert account.init_health_ratio(frame) == Decimal("4.52652018845639596719637165673707200") + assert account.init_health_ratio(frame) == Decimal( + "4.52652018845639596719637165673707200" + ) # Typescript says: 9.50397353076404272088 - assert account.maint_health_ratio(frame) == Decimal("9.50397353076384339420572268801026600") + assert account.maint_health_ratio(frame) == Decimal( + "9.50397353076384339420572268801026600" + ) # Typescript says: 1025929.00722205438034961844 - assert account.total_value(frame) == Decimal("1025929.00722286391011661795227041613") + assert account.total_value(frame) == Decimal( + "1025929.00722286391011661795227041613" + ) # Typescript says: 6.50157472788435697453 assert account.leverage(frame) == Decimal("6.50157472787922998475118662978475117") @@ -129,20 +169,30 @@ def test_account3() -> None: def test_account4() -> None: - group, cache, account, open_orders = load_data_from_directory("tests/testdata/account4") + group, cache, account, open_orders = load_data_from_directory( + "tests/testdata/account4" + ) frame = account.to_dataframe(group, open_orders, cache) # Typescript says: -848086876487.04950427436299875694 - assert account.init_health(frame) == Decimal("-848086.87648706716344229365643382143") + assert account.init_health(frame) == Decimal( + "-848086.87648706716344229365643382143" + ) # Typescript says: -433869053006.07361789143756070075 - assert account.maint_health(frame) == Decimal("-433869.05300609716344867811565222143") + assert account.maint_health(frame) == Decimal( + "-433869.05300609716344867811565222143" + ) # Typescript says: -9.30655353087566084014 - assert account.init_health_ratio(frame) == Decimal("-9.30655353087574422134411842983207950") + assert account.init_health_ratio(frame) == Decimal( + "-9.30655353087574422134411842983207950" + ) # Typescript says: -4.98781798472691662028 - assert account.maint_health_ratio(frame) == Decimal("-4.98781798472697013664621930744313090") + assert account.maint_health_ratio(frame) == Decimal( + "-4.98781798472697013664621930744313090" + ) # Typescript says: -19651.22952604663374742699 assert account.total_value(frame) == Decimal("-19651.22952512716345506257487062143") @@ -154,23 +204,35 @@ def test_account4() -> None: def test_account5() -> None: - group, cache, account, open_orders = load_data_from_directory("tests/testdata/account5") + group, cache, account, open_orders = load_data_from_directory( + "tests/testdata/account5" + ) frame = account.to_dataframe(group, open_orders, cache) # Typescript says: 15144959918141.09175135195858530324 - assert account.init_health(frame) == Decimal("15144959.9181410924496111317578727438") + assert account.init_health(frame) == Decimal( + "15144959.9181410924496111317578727438" + ) # Typescript says: 15361719060997.68276021614036608298 - assert account.maint_health(frame) == Decimal("15361719.0609976820704356689151633723") + assert account.maint_health(frame) == Decimal( + "15361719.0609976820704356689151633723" + ) # Typescript says: 878.88913077823325181726 - assert account.init_health_ratio(frame) == Decimal("878.889130778232107967643669989641770") + assert account.init_health_ratio(frame) == Decimal( + "878.889130778232107967643669989641770" + ) # Typescript says: 946.44498820888003365326 - assert account.maint_health_ratio(frame) == Decimal("946.444988208877836980861464823408690") + assert account.maint_health_ratio(frame) == Decimal( + "946.444988208877836980861464823408690" + ) # Typescript says: 15578478.17337437202354522015 - assert account.total_value(frame) == Decimal("15578478.2038542716912602060724540009") + assert account.total_value(frame) == Decimal( + "15578478.2038542716912602060724540009" + ) # Typescript says: 0.09884076560217636143 assert account.leverage(frame) == Decimal("0.0988407635236814851193291170841739152") @@ -179,23 +241,35 @@ def test_account5() -> None: def test_account6() -> None: - group, cache, account, open_orders = load_data_from_directory("tests/testdata/account6") + group, cache, account, open_orders = load_data_from_directory( + "tests/testdata/account6" + ) frame = account.to_dataframe(group, open_orders, cache) # Typescript says: 14480970069238.33686487450164648294 - assert account.init_health(frame) == Decimal("14480970.0692383312566073701425189089") + assert account.init_health(frame) == Decimal( + "14480970.0692383312566073701425189089" + ) # Typescript says: 15030566.251990.17026082618337312624 - assert account.maint_health(frame) == Decimal("15030566.2519901615291113644851708626") + assert account.maint_health(frame) == Decimal( + "15030566.2519901615291113644851708626" + ) # Typescript says: 215.03167137712999590349 - assert account.init_health_ratio(frame) == Decimal("215.031671377129731140294440951729501") + assert account.init_health_ratio(frame) == Decimal( + "215.031671377129731140294440951729501" + ) # Typescript says: 236.77769605824430243501 - assert account.maint_health_ratio(frame) == Decimal("236.777696058243876968239282498979720") + assert account.maint_health_ratio(frame) == Decimal( + "236.777696058243876968239282498979720" + ) # Typescript says: 15580162.40781940827396567784 - assert account.total_value(frame) == Decimal("15580162.4347419918016153588278228163") + assert account.total_value(frame) == Decimal( + "15580162.4347419918016153588278228163" + ) # Typescript says: 0.07913870989902704878 assert account.leverage(frame) == Decimal("0.0791387081247153498553556005902933099") @@ -204,23 +278,35 @@ def test_account6() -> None: def test_account7() -> None: - group, cache, account, open_orders = load_data_from_directory("tests/testdata/account7") + group, cache, account, open_orders = load_data_from_directory( + "tests/testdata/account7" + ) frame = account.to_dataframe(group, open_orders, cache) # Typescript says: 16272272.28055547965738014682 - assert account.init_health(frame) == Decimal("16.2722722805554769752688793558604253") + assert account.init_health(frame) == Decimal( + "16.2722722805554769752688793558604253" + ) # Typescript says: 16649749.17384252860704663135 - assert account.maint_health(frame) == Decimal("16.6497491738425205606631394095387232") + assert account.maint_health(frame) == Decimal( + "16.6497491738425205606631394095387232" + ) # Typescript says: 359.23329723261616663876 - assert account.init_health_ratio(frame) == Decimal("359.233297232615934356690098616636253") + assert account.init_health_ratio(frame) == Decimal( + "359.233297232615934356690098616636253" + ) # Typescript says: 400.98177879921834687593 - assert account.maint_health_ratio(frame) == Decimal("400.981778799217382934571016672694094") + assert account.maint_health_ratio(frame) == Decimal( + "400.981778799217382934571016672694094" + ) # Typescript says: 17.02722595090433088671 - assert account.total_value(frame) == Decimal("17.0272260671295641460573994632170211") + assert account.total_value(frame) == Decimal( + "17.0272260671295641460573994632170211" + ) # Typescript says: 0.22169019545401269511 assert account.leverage(frame) == Decimal("0.221690187114945806055453687883677967") diff --git a/tests/test_instructions.py b/tests/test_instructions.py index 354c836..cc97475 100644 --- a/tests/test_instructions.py +++ b/tests/test_instructions.py @@ -1,7 +1,13 @@ import typing from .context import mango -from .fakes import fake_context, fake_market, fake_seeded_public_key, fake_token, fake_wallet +from .fakes import ( + fake_context, + fake_market, + fake_seeded_public_key, + fake_token, + fake_wallet, +) from decimal import Decimal from pyserum.market.market import Market as PySerumMarket @@ -27,7 +33,9 @@ def test_build_create_associated_spl_account_instructions() -> None: context: mango.Context = fake_context() wallet: mango.Wallet = fake_wallet() token: mango.Token = fake_token() - actual = mango.build_create_associated_spl_account_instructions(context, wallet, token) + actual = mango.build_create_associated_spl_account_instructions( + context, wallet, token + ) assert actual is not None assert len(actual.signers) == 0 assert len(actual.instructions) == 1 @@ -42,7 +50,9 @@ def test_build_transfer_spl_tokens_instructions() -> None: source: PublicKey = fake_seeded_public_key("source SPL account") destination: PublicKey = fake_seeded_public_key("destination SPL account") quantity: Decimal = Decimal(7) - actual = mango.build_transfer_spl_tokens_instructions(context, wallet, token, source, destination, quantity) + actual = mango.build_transfer_spl_tokens_instructions( + context, wallet, token, source, destination, quantity + ) assert actual is not None assert len(actual.signers) == 0 assert len(actual.instructions) == 1 @@ -87,7 +97,18 @@ def test_build_serum_place_order_instructions() -> None: client_id: int = 53 fee_discount_address: PublicKey = fake_seeded_public_key("fee discount address") actual = mango.build_serum_place_order_instructions( - context, wallet, market, source, open_orders_address, order_type, side, price, quantity, client_id, fee_discount_address) + context, + wallet, + market, + source, + open_orders_address, + order_type, + side, + price, + quantity, + client_id, + fee_discount_address, + ) assert actual is not None assert len(actual.signers) == 0 assert len(actual.instructions) == 1 @@ -99,10 +120,13 @@ def test_build_serum_consume_events_instructions() -> None: context: mango.Context = fake_context() market_address: PublicKey = fake_seeded_public_key("market address") event_queue_address: PublicKey = fake_seeded_public_key("event queue address") - open_orders_addresses: typing.Sequence[PublicKey] = [fake_seeded_public_key("open orders account")] + open_orders_addresses: typing.Sequence[PublicKey] = [ + fake_seeded_public_key("open orders account") + ] limit: int = 64 actual = mango.build_serum_consume_events_instructions( - context, market_address, event_queue_address, open_orders_addresses, limit) + context, market_address, event_queue_address, open_orders_addresses, limit + ) assert actual is not None assert len(actual.signers) == 0 assert len(actual.instructions) == 1 @@ -116,9 +140,17 @@ def test_build_serum_settle_instructions() -> None: wallet: mango.Wallet = fake_wallet() open_orders_address: PublicKey = fake_seeded_public_key("open orders account") base_token_account_address: PublicKey = fake_seeded_public_key("base token account") - quote_token_account_address: PublicKey = fake_seeded_public_key("quote token account") + quote_token_account_address: PublicKey = fake_seeded_public_key( + "quote token account" + ) actual = mango.build_serum_settle_instructions( - context, wallet, market, open_orders_address, base_token_account_address, quote_token_account_address) + context, + wallet, + market, + open_orders_address, + base_token_account_address, + quote_token_account_address, + ) assert actual is not None assert len(actual.signers) == 0 assert len(actual.instructions) == 1 diff --git a/tests/test_instrumentlookup.py b/tests/test_instrumentlookup.py index cf81252..d26f967 100644 --- a/tests/test_instrumentlookup.py +++ b/tests/test_instrumentlookup.py @@ -29,7 +29,8 @@ def test_spl_token_lookup() -> None: "symbol": "ETH", "name": "Wrapped Ethereum (Sollet)", "decimals": 6, - }] + }, + ] } actual = mango.SPLTokenLookup("test-filename", data) assert actual is not None @@ -52,7 +53,9 @@ def test_spl_token_lookups_with_full_data() -> None: srm = actual.find_by_mint(PublicKey("AKJHspCwDhABucCxNLXUSfEzb7Ny62RqFtC9uNjJi4fq")) assert srm is not None assert srm.symbol == "SRM-SOL" - usdt = actual.find_by_mint(PublicKey("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB")) + usdt = actual.find_by_mint( + PublicKey("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB") + ) assert usdt is not None assert usdt.symbol == "USDT" @@ -62,7 +65,9 @@ def test_override_lookups_with_full_data() -> None: eth = actual.find_by_symbol("ETH") assert eth is not None assert eth.mint == PublicKey("2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk") - usdt = actual.find_by_mint(PublicKey("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB")) + usdt = actual.find_by_mint( + PublicKey("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB") + ) assert usdt is not None assert usdt.symbol == "USDT" @@ -84,7 +89,9 @@ def test_compound_lookups_with_full_data() -> None: assert srm is not None assert isinstance(srm, mango.Token) assert srm.symbol == "SRM-SOL" - usdt = actual.find_by_mint(PublicKey("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB")) + usdt = actual.find_by_mint( + PublicKey("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB") + ) assert usdt is not None assert isinstance(usdt, mango.Token) assert usdt.symbol == "USDT" diff --git a/tests/test_liquidationevent.py b/tests/test_liquidationevent.py index 559ede3..e94600c 100644 --- a/tests/test_liquidationevent.py +++ b/tests/test_liquidationevent.py @@ -10,18 +10,28 @@ def test_liquidation_event() -> None: balances_before = [ mango.InstrumentValue(fake_token("ETH"), Decimal(1)), mango.InstrumentValue(fake_token("BTC"), Decimal("0.1")), - mango.InstrumentValue(fake_token("USDT"), Decimal(1000)) + mango.InstrumentValue(fake_token("USDT"), Decimal(1000)), ] balances_after = [ mango.InstrumentValue(fake_token("ETH"), Decimal(1)), mango.InstrumentValue(fake_token("BTC"), Decimal("0.05")), - mango.InstrumentValue(fake_token("USDT"), Decimal(2000)) + mango.InstrumentValue(fake_token("USDT"), Decimal(2000)), ] timestamp = datetime.datetime(2021, 5, 17, 12, 20, 56) - event = mango.LiquidationEvent(timestamp, "Liquidator", "Group", True, ["signature"], - fake_public_key(), fake_public_key(), - balances_before, balances_after) - assert str(event) == """ยซ ๐Ÿฅญ Liqudation Event โœ… at 2021-05-17 12:20:56 + event = mango.LiquidationEvent( + timestamp, + "Liquidator", + "Group", + True, + ["signature"], + fake_public_key(), + fake_public_key(), + balances_before, + balances_after, + ) + assert ( + str(event) + == """ยซ ๐Ÿฅญ Liqudation Event โœ… at 2021-05-17 12:20:56 ๐Ÿ’ง Liquidator: Liquidator ๐Ÿซ Group: Group ๐Ÿ“‡ Signatures: ['signature'] @@ -32,3 +42,4 @@ def test_liquidation_event() -> None: -0.05000000 BTC 1,000.00000000 USDT ยป""" + ) diff --git a/tests/test_logmessages.py b/tests/test_logmessages.py index a671f38..7ce513d 100644 --- a/tests/test_logmessages.py +++ b/tests/test_logmessages.py @@ -10,7 +10,7 @@ def test_no_messages_to_expand() -> None: "Program DESVgJVGajEgKGXhb6XmqDHGz3VjdgP7rEVESBgxmroY consumed 8858 of 171625 compute units", "Program DESVgJVGajEgKGXhb6XmqDHGz3VjdgP7rEVESBgxmroY failed: custom program error: 0x2a", "Program 4skJ85cdxQAFVKbcGgfun8iZPL7BadVYXG3kGEGkufqA consumed 200000 of 200000 compute units", - "Program 4skJ85cdxQAFVKbcGgfun8iZPL7BadVYXG3kGEGkufqA failed: custom program error: 0x2a" + "Program 4skJ85cdxQAFVKbcGgfun8iZPL7BadVYXG3kGEGkufqA failed: custom program error: 0x2a", ] actual = mango.expand_log_messages(logs) assert len(actual) == 7 @@ -25,14 +25,16 @@ def test_expand_liquidate_perp_market() -> None: "Program log: mango-log", "Program log: xL0/TYaKkmo9V1sXbGlWtx7PorbATlnhud1k4TouaelSIuWjq6DS+naor4jdUZPAHrtSr/wNa5D+q2Ybbpli42dDOOeJCluKHCjgTI66neHYoNpbISs2BljP2rJh/YYyevMmtXuMZigBAAAAAAAAAAAAAAAAAJg6AAAAAAAAAAAKAAAAAAAAAMDGLQAAAPCPJv////////8A", "Program 4skJ85cdxQAFVKbcGgfun8iZPL7BadVYXG3kGEGkufqA consumed 24022 of 200000 compute units", - "Program 4skJ85cdxQAFVKbcGgfun8iZPL7BadVYXG3kGEGkufqA success" + "Program 4skJ85cdxQAFVKbcGgfun8iZPL7BadVYXG3kGEGkufqA success", ] actual = mango.expand_log_messages(logs) assert len(actual) == 5 assert actual[0] == logs[0] assert actual[1] == logs[1] - assert actual[2] == """Mango LiquidatePerpMarketLog Container: + assert ( + actual[2] + == """Mango LiquidatePerpMarketLog Container: mangoGroup = 58T8PuaCBa6FqFqcoTB2Ay6snLp2gAUxU8hnDWcLFqyB liqee = 8zCJ6jdHExdnNb17cxFhFtavZ7uaRHXj1nbT3VJ8E2i5 liqor = 2tvZs8riWYKDWsGMoNxVy1YDc7qtJb4EurcfpB2PBfKm @@ -41,6 +43,7 @@ def test_expand_liquidate_perp_market() -> None: baseTransfer = 10 quoteTransfer = -4011018418126845000000 bankruptcy = False""" + ) assert actual[3] == logs[4] assert actual[4] == logs[5] @@ -57,26 +60,34 @@ def test_expand_liquidate_token_and_perp() -> None: "Program log: mango-log", "Program log: EmyboIQCGyA9V1sXbGlWtx7PorbATlnhud1k4TouaelSIuWjq6DS+naor4jdUZPAHrtSr/wNa5D+q2Ybbpli42dDOOeJCluKHCjgTI66neHYoNpbISs2BljP2rJh/YYyevMmtXuMZigPAAAAAAAAAAEAAAAAAAAAAAEAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAICWmAAAAAAAAAAAAAAAAACAlpgAAAAAAAAAAQ==", "Program 4skJ85cdxQAFVKbcGgfun8iZPL7BadVYXG3kGEGkufqA consumed 34000 of 200000 compute units", - "Program 4skJ85cdxQAFVKbcGgfun8iZPL7BadVYXG3kGEGkufqA success" + "Program 4skJ85cdxQAFVKbcGgfun8iZPL7BadVYXG3kGEGkufqA success", ] actual = mango.expand_log_messages(logs) assert len(actual) == 7 assert actual[0] == logs[0] assert actual[1] == logs[1] - assert actual[2] == """Mango TokenBalanceLog Container: + assert ( + actual[2] + == """Mango TokenBalanceLog Container: mangoGroup = 58T8PuaCBa6FqFqcoTB2Ay6snLp2gAUxU8hnDWcLFqyB mangoAccount = 2tvZs8riWYKDWsGMoNxVy1YDc7qtJb4EurcfpB2PBfKm tokenIndex = 15 deposit = 284289726477762560 borrow = 0""" - assert actual[3] == """Mango TokenBalanceLog Container: + ) + assert ( + actual[3] + == """Mango TokenBalanceLog Container: mangoGroup = 58T8PuaCBa6FqFqcoTB2Ay6snLp2gAUxU8hnDWcLFqyB mangoAccount = 8zCJ6jdHExdnNb17cxFhFtavZ7uaRHXj1nbT3VJ8E2i5 tokenIndex = 15 deposit = 0 borrow = 0""" - assert actual[4] == """Mango LiquidateTokenAndPerpLog Container: + ) + assert ( + actual[4] + == """Mango LiquidateTokenAndPerpLog Container: mangoGroup = 58T8PuaCBa6FqFqcoTB2Ay6snLp2gAUxU8hnDWcLFqyB liqee = 8zCJ6jdHExdnNb17cxFhFtavZ7uaRHXj1nbT3VJ8E2i5 liqor = 2tvZs8riWYKDWsGMoNxVy1YDc7qtJb4EurcfpB2PBfKm @@ -89,6 +100,7 @@ def test_expand_liquidate_token_and_perp() -> None: assetTransfer = 2814749767106560000000 liabTransfer = 2814749767106560000000 bankruptcy = True""" + ) assert actual[5] == logs[8] assert actual[6] == logs[9] @@ -101,14 +113,16 @@ def test_expand_resolve_perp_bankruptcy() -> None: "Program log: mango-log", "Program log: ZS+iIbP3D4M9V1sXbGlWtx7PorbATlnhud1k4TouaelSIuWjq6DS+naor4jdUZPAHrtSr/wNa5D+q2Ybbpli42dDOOeJCluKHCjgTI66neHYoNpbISs2BljP2rJh/YYyevMmtXuMZigBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANkZCAAAkOj////////////ZGQgAAJDo////////////", "Program 4skJ85cdxQAFVKbcGgfun8iZPL7BadVYXG3kGEGkufqA consumed 11097 of 200000 compute units", - "Program 4skJ85cdxQAFVKbcGgfun8iZPL7BadVYXG3kGEGkufqA success" + "Program 4skJ85cdxQAFVKbcGgfun8iZPL7BadVYXG3kGEGkufqA success", ] actual = mango.expand_log_messages(logs) assert len(actual) == 5 assert actual[0] == logs[0] assert actual[1] == logs[1] - assert actual[2] == """Mango PerpBankruptcyLog Container: + assert ( + actual[2] + == """Mango PerpBankruptcyLog Container: mangoGroup = 58T8PuaCBa6FqFqcoTB2Ay6snLp2gAUxU8hnDWcLFqyB liqee = 8zCJ6jdHExdnNb17cxFhFtavZ7uaRHXj1nbT3VJ8E2i5 liqor = 2tvZs8riWYKDWsGMoNxVy1YDc7qtJb4EurcfpB2PBfKm @@ -117,6 +131,7 @@ def test_expand_resolve_perp_bankruptcy() -> None: socializedLoss = 0 cacheLongFunding = -6597069766125095 cacheShortFunding = -6597069766125095""" + ) assert actual[3] == logs[4] assert actual[4] == logs[5] @@ -141,14 +156,16 @@ def test_expand_caches() -> None: "Program log: mango-log", "Program log: 9lt9JxCzqw543ogDQe0xql0u2Ff66cc9XSGzkyyZdqoGDHUXDByC3QIAAAABAAAAAAAAAAMAAAAAAAAAAgAAAMqkWEieuOP5BAAAAAAAAAAMaEaZks1h/QAAAAAAAAAAAgAAAFDep7S5sr/zBAAAAAAAAAAMaEaZks1h/QAAAAAAAAAA", "Program mv3ekLzLbnVPNxjSKvqBpU3ZeZXPQdEC3bp5MDEBG68 consumed 5723 of 200000 compute units", - "Program mv3ekLzLbnVPNxjSKvqBpU3ZeZXPQdEC3bp5MDEBG68 success" + "Program mv3ekLzLbnVPNxjSKvqBpU3ZeZXPQdEC3bp5MDEBG68 success", ] actual = mango.expand_log_messages(logs) assert len(actual) == 15 assert actual[0] == logs[0] assert actual[1] == logs[1] - assert actual[2] == """Mango CacheRootBanksLog Container: + assert ( + actual[2] + == """Mango CacheRootBanksLog Container: mangoGroup = 98pjRuQjK3qA6gXts96PqZT4Ze5QmnCmt3QYjhbUSPue tokenIndexes_count = 8 tokenIndexes = ListContainer: @@ -180,11 +197,14 @@ def test_expand_caches() -> None: 285115763349990654498 285473328472584674585 281912611517885852855""" + ) assert actual[3] == logs[4] assert actual[4] == logs[5] assert actual[5] == logs[6] assert actual[6] == logs[7] - assert actual[7] == """Mango CachePricesLog Container: + assert ( + actual[7] + == """Mango CachePricesLog Container: mangoGroup = 98pjRuQjK3qA6gXts96PqZT4Ze5QmnCmt3QYjhbUSPue oracleIndexes_count = 8 oracleIndexes = ListContainer: @@ -206,11 +226,14 @@ def test_expand_caches() -> None: 2138151364498562 2782752451736529 380725026638420""" + ) assert actual[8] == logs[10] assert actual[9] == logs[11] assert actual[10] == logs[12] assert actual[11] == logs[13] - assert actual[12] == """Mango CachePerpMarketsLog Container: + assert ( + actual[12] + == """Mango CachePerpMarketsLog Container: mangoGroup = 98pjRuQjK3qA6gXts96PqZT4Ze5QmnCmt3QYjhbUSPue marketIndexes_count = 2 marketIndexes = ListContainer: @@ -224,5 +247,6 @@ def test_expand_caches() -> None: shortFundings = ListContainer: 91350929877276024400 18258100393857148940""" + ) assert actual[13] == logs[16] assert actual[14] == logs[17] diff --git a/tests/test_lotsizeconverter.py b/tests/test_lotsizeconverter.py index 1762fb4..a7fcade 100644 --- a/tests/test_lotsizeconverter.py +++ b/tests/test_lotsizeconverter.py @@ -50,7 +50,9 @@ def test_round_base_sol() -> None: fake_quote = fake_token("USDC") # From SOL/USDC on Mango spot: # ยซ ๐™ป๐š˜๐š๐š‚๐š’๐šฃ๐šŽ๐™ฒ๐š˜๐š—๐šŸ๐šŽ๐š›๐š๐šŽ๐š› SOL/USDC [base lot size: 100000000 (9 decimals), quote lot size: 100 (6 decimals)] ยป - sut = mango.LotSizeConverter(fake_base, Decimal(100000000), fake_quote, Decimal(100)) + sut = mango.LotSizeConverter( + fake_base, Decimal(100000000), fake_quote, Decimal(100) + ) actual = sut.round_base(Decimal("1234567890.1234567890")) assert actual == Decimal("1234567890.1") @@ -120,7 +122,9 @@ def test_round_quote_sol() -> None: fake_quote = fake_token("USDC") # From SOL/USDC on Mango spot: # ยซ ๐™ป๐š˜๐š๐š‚๐š’๐šฃ๐šŽ๐™ฒ๐š˜๐š—๐šŸ๐šŽ๐š›๐š๐šŽ๐š› SOL/USDC [base lot size: 100000000 (9 decimals), quote lot size: 100 (6 decimals)] ยป - sut = mango.LotSizeConverter(fake_base, Decimal(100000000), fake_quote, Decimal(100)) + sut = mango.LotSizeConverter( + fake_base, Decimal(100000000), fake_quote, Decimal(100) + ) actual = sut.round_quote(Decimal("1234567890.1234567890")) assert actual == Decimal("1234567890.123") diff --git a/tests/test_marketlookup.py b/tests/test_marketlookup.py index cc872de..c930c11 100644 --- a/tests/test_marketlookup.py +++ b/tests/test_marketlookup.py @@ -28,8 +28,8 @@ def test_serum_market_lookup() -> None: "website": "https://solana.com/", "serumV3Usdc": "9wFFyRfZBsuAha4YcuxcXLKwMxJR43S7fPfQLusDBzvT", "serumV3Usdt": "HWHvQhFmJB3NUcu1aihKmrKegfVxBEHzwVX6yZCKEsi1", - "coingeckoId": "solana" - } + "coingeckoId": "solana", + }, }, { "chainId": 101, @@ -38,13 +38,11 @@ def test_serum_market_lookup() -> None: "name": "USD Coin", "decimals": 6, "logoURI": "https://cdn.jsdelivr.net/gh/solana-labs/token-list@main/assets/mainnet/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v/logo.png", - "tags": [ - "stablecoin" - ], + "tags": ["stablecoin"], "extensions": { "website": "https://www.centre.io/", - "coingeckoId": "usd-coin" - } + "coingeckoId": "usd-coin", + }, }, { "chainId": 101, @@ -53,16 +51,13 @@ def test_serum_market_lookup() -> None: "name": "Wrapped Bitcoin (Sollet)", "decimals": 6, "logoURI": "https://cdn.jsdelivr.net/gh/trustwallet/assets@master/blockchains/bitcoin/info/logo.png", - "tags": [ - "wrapped-sollet", - "ethereum" - ], + "tags": ["wrapped-sollet", "ethereum"], "extensions": { "bridgeContract": "https://etherscan.io/address/0xeae57ce9cc1984f202e15e038b964bb8bdf7229a", "serumV3Usdc": "A8YFbxQYFVqKZaoYJLLUVcQiWP7G2MeEgW5wsAQgMvFw", "serumV3Usdt": "C1EuT9VokAKLiW7i2ASnZUvxDoKuKkCpDDeNxAptuNe4", - "coingeckoId": "bitcoin" - } + "coingeckoId": "bitcoin", + }, }, { "chainId": 101, @@ -71,16 +66,13 @@ def test_serum_market_lookup() -> None: "name": "Wrapped Ethereum (Sollet)", "decimals": 6, "logoURI": "https://cdn.jsdelivr.net/gh/trustwallet/assets@master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", - "tags": [ - "wrapped-sollet", - "ethereum" - ], + "tags": ["wrapped-sollet", "ethereum"], "extensions": { "bridgeContract": "https://etherscan.io/address/0xeae57ce9cc1984f202e15e038b964bb8bdf7229a", "serumV3Usdc": "4tSvZvnbyzHXLMTiFonMyxZoHmFqau1XArcRCVHLZ5gX", "serumV3Usdt": "7dLVkUfBVfCGkFhSXDCq1ukM9usathSgS716t643iFGF", - "coingeckoId": "ethereum" - } + "coingeckoId": "ethereum", + }, }, { "chainId": 101, @@ -89,13 +81,11 @@ def test_serum_market_lookup() -> None: "name": "USDT", "decimals": 6, "logoURI": "https://cdn.jsdelivr.net/gh/solana-labs/explorer/public/tokens/usdt.svg", - "tags": [ - "stablecoin" - ], + "tags": ["stablecoin"], "extensions": { "website": "https://tether.to/", - "coingeckoId": "tether" - } + "coingeckoId": "tether", + }, }, { "chainId": 101, @@ -104,14 +94,12 @@ def test_serum_market_lookup() -> None: "name": "USD Coin", "decimals": 6, "logoURI": "https://cdn.jsdelivr.net/gh/solana-labs/token-list@main/assets/mainnet/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v/logo.png", - "tags": [ - "stablecoin" - ], + "tags": ["stablecoin"], "extensions": { "website": "https://www.centre.io/", - "coingeckoId": "usd-coin" - } - } + "coingeckoId": "usd-coin", + }, + }, ] } actual = mango.SerumMarketLookup(fake_seeded_public_key("program ID"), data) @@ -125,8 +113,9 @@ def test_serum_market_lookup() -> None: def test_serum_market_lookups_with_full_data() -> None: - market_lookup = mango.SerumMarketLookup.load(fake_seeded_public_key( - "program ID"), mango.SPLTokenLookup.DefaultDataFilepath) + market_lookup = mango.SerumMarketLookup.load( + fake_seeded_public_key("program ID"), mango.SPLTokenLookup.DefaultDataFilepath + ) srm_usdc = market_lookup.find_by_symbol("SRM/USDC") assert srm_usdc is not None assert srm_usdc.base.symbol == "SRM" @@ -145,8 +134,9 @@ def test_serum_market_lookups_with_full_data() -> None: def test_serum_market_case_insensitive_lookups_with_full_data() -> None: - market_lookup = mango.SerumMarketLookup.load(fake_seeded_public_key( - "program ID"), mango.SPLTokenLookup.DefaultDataFilepath) + market_lookup = mango.SerumMarketLookup.load( + fake_seeded_public_key("program ID"), mango.SPLTokenLookup.DefaultDataFilepath + ) srm_usdc = market_lookup.find_by_symbol("srm/usdc") assert srm_usdc is not None assert srm_usdc.base.symbol == "SRM" @@ -155,8 +145,9 @@ def test_serum_market_case_insensitive_lookups_with_full_data() -> None: def test_overrides_with_full_data() -> None: - market_lookup = mango.SerumMarketLookup.load(fake_seeded_public_key( - "program ID"), "./data/overrides.tokenlist.json") + market_lookup = mango.SerumMarketLookup.load( + fake_seeded_public_key("program ID"), "./data/overrides.tokenlist.json" + ) eth_usdt = market_lookup.find_by_symbol("ETH/USDT") assert eth_usdt is not None assert eth_usdt.base.symbol == "ETH" @@ -175,10 +166,12 @@ def test_overrides_with_full_data() -> None: def test_compound_lookups_with_full_data() -> None: - overrides = mango.SerumMarketLookup.load(fake_seeded_public_key( - "program ID"), "./data/overrides.tokenlist.json") - spl = mango.SerumMarketLookup.load(fake_seeded_public_key( - "program ID"), mango.SPLTokenLookup.DefaultDataFilepath) + overrides = mango.SerumMarketLookup.load( + fake_seeded_public_key("program ID"), "./data/overrides.tokenlist.json" + ) + spl = mango.SerumMarketLookup.load( + fake_seeded_public_key("program ID"), mango.SPLTokenLookup.DefaultDataFilepath + ) actual = mango.CompoundMarketLookup([overrides, spl]) # actual should now find instruments in either overrides or spl eth_usdt = actual.find_by_symbol("ETH/USDT") @@ -198,13 +191,17 @@ def test_compound_lookups_with_full_data() -> None: assert srm_usdc is not None assert srm_usdc.base.symbol == "SRM" assert srm_usdc.quote.symbol == "USDC" - assert srm_usdc.address == PublicKey("ByRys5tuUWDgL73G8JBAEfkdFf8JWBzPBDHsBVQ5vbQA") + assert srm_usdc.address == PublicKey( + "ByRys5tuUWDgL73G8JBAEfkdFf8JWBzPBDHsBVQ5vbQA" + ) btc_usdc = actual.find_by_symbol("BTC/USDC") assert btc_usdc is not None assert btc_usdc.base.symbol == "BTC" assert btc_usdc.quote.symbol == "USDC" - assert btc_usdc.address == PublicKey("A8YFbxQYFVqKZaoYJLLUVcQiWP7G2MeEgW5wsAQgMvFw") + assert btc_usdc.address == PublicKey( + "A8YFbxQYFVqKZaoYJLLUVcQiWP7G2MeEgW5wsAQgMvFw" + ) non_existant_market = actual.find_by_symbol("ETH/BTC") assert non_existant_market is None # No such market diff --git a/tests/test_notification.py b/tests/test_notification.py index a4bf697..c57bd9e 100644 --- a/tests/test_notification.py +++ b/tests/test_notification.py @@ -62,6 +62,7 @@ def test_filtering_notification_target_constructor() -> None: def func(_: typing.Any) -> bool: return True + actual = mango.FilteringNotificationTarget(mock, func) assert actual is not None assert actual.inner_notifier == mock @@ -72,22 +73,25 @@ def test_filtering_notification_target() -> None: mock = MockNotificationTarget() filtering = mango.FilteringNotificationTarget(mock, lambda x: bool(x == "yes")) filtering.send("no") - assert(not mock.send_notification_called) + assert not mock.send_notification_called filtering.send("yes") - assert(mock.send_notification_called) + assert mock.send_notification_called def test_parse_notification_target() -> None: telegram_target = mango.parse_notification_target( - "telegram:012345678@9876543210:ABCDEFGHijklmnop-qrstuvwxyzABCDEFGH") + "telegram:012345678@9876543210:ABCDEFGHijklmnop-qrstuvwxyzABCDEFGH" + ) assert telegram_target is not None discord_target = mango.parse_notification_target( - "discord:https://discord.com/api/webhooks/012345678901234567/ABCDE_fghij-KLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN") + "discord:https://discord.com/api/webhooks/012345678901234567/ABCDE_fghij-KLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN" + ) assert discord_target is not None mailjet_target = mango.parse_notification_target( - "mailjet:user:secret:subject:from%20name:from@address:to%20name%20with%20colon%3A:to@address") + "mailjet:user:secret:subject:from%20name:from@address:to%20name%20with%20colon%3A:to@address" + ) assert mailjet_target is not None csvfile_target = mango.parse_notification_target("csvfile:filename.csv") diff --git a/tests/test_openorders.py b/tests/test_openorders.py index ec7b607..b96b38e 100644 --- a/tests/test_openorders.py +++ b/tests/test_openorders.py @@ -10,7 +10,21 @@ def test_constructor() -> None: market = fake_public_key() owner = fake_public_key() - flags = mango.AccountFlags(mango.Version.V1, True, False, True, False, False, False, False, False) - actual = mango.OpenOrders(account_info, mango.Version.V1, program_address, flags, market, - owner, Decimal(0), Decimal(0), Decimal(0), Decimal(0), [], Decimal(0)) + flags = mango.AccountFlags( + mango.Version.V1, True, False, True, False, False, False, False, False + ) + actual = mango.OpenOrders( + account_info, + mango.Version.V1, + program_address, + flags, + market, + owner, + Decimal(0), + Decimal(0), + Decimal(0), + Decimal(0), + [], + Decimal(0), + ) assert actual is not None diff --git a/tests/test_orderbook.py b/tests/test_orderbook.py index 5f0c353..6d34f74 100644 --- a/tests/test_orderbook.py +++ b/tests/test_orderbook.py @@ -48,7 +48,9 @@ def test_orderbook_spread() -> None: # ASK is SELL, BID is BUY -def _construct_order_book_side(askOrBidSide: mango.Side, size: int) -> typing.Sequence[mango.Order]: +def _construct_order_book_side( + askOrBidSide: mango.Side, size: int +) -> typing.Sequence[mango.Order]: result_orders: typing.List[mango.Order] = [] for index, price in enumerate(random.sample(range(1, 1000), size)): constructed_id = fake_order_id(index, price) @@ -64,15 +66,14 @@ def _construct_order_book_side(askOrBidSide: mango.Side, size: int) -> typing.Se def _construct_order_book( - bids: typing.Sequence[mango.Order], - asks: typing.Sequence[mango.Order] + bids: typing.Sequence[mango.Order], asks: typing.Sequence[mango.Order] ) -> mango.OrderBook: # construct orderbook return mango.OrderBook( - symbol='TEST', + symbol="TEST", lot_size_converter=mango.NullLotSizeConverter(), bids=bids, - asks=asks + asks=asks, ) diff --git a/tests/test_perpeventqueue.py b/tests/test_perpeventqueue.py index 8a6b38e..50c54af 100644 --- a/tests/test_perpeventqueue.py +++ b/tests/test_perpeventqueue.py @@ -12,15 +12,25 @@ from decimal import Decimal def test_constructor() -> None: address = fake_seeded_public_key("perp event queue address") account_info: mango.AccountInfo = fake_account_info(address) - meta_data: mango.Metadata = mango.Metadata(mango.layouts.DATA_TYPE.EventQueue, mango.Version.V1, True) + meta_data: mango.Metadata = mango.Metadata( + mango.layouts.DATA_TYPE.EventQueue, mango.Version.V1, True + ) head: Decimal = Decimal(0) count: Decimal = Decimal(0) sequence_number: Decimal = Decimal(0) unprocessed_events: typing.Sequence[mango.PerpEvent] = [] processed_events: typing.Sequence[mango.PerpEvent] = [] - actual = mango.PerpEventQueue(account_info, mango.Version.V1, meta_data, head, count, - sequence_number, unprocessed_events, processed_events) + actual = mango.PerpEventQueue( + account_info, + mango.Version.V1, + meta_data, + head, + count, + sequence_number, + unprocessed_events, + processed_events, + ) assert actual is not None assert actual.account_info == account_info assert actual.meta_data == meta_data @@ -32,11 +42,28 @@ def test_constructor() -> None: assert actual.processed_events == processed_events -def _fake_pev(head: Decimal, count: Decimal, sequence_number: Decimal, unprocessed: typing.Sequence[mango.PerpEvent], processed: typing.Sequence[mango.PerpEvent]) -> mango.PerpEventQueue: +def _fake_pev( + head: Decimal, + count: Decimal, + sequence_number: Decimal, + unprocessed: typing.Sequence[mango.PerpEvent], + processed: typing.Sequence[mango.PerpEvent], +) -> mango.PerpEventQueue: address = fake_seeded_public_key("perp event queue address") account_info: mango.AccountInfo = fake_account_info(address) - meta_data: mango.Metadata = mango.Metadata(mango.layouts.DATA_TYPE.EventQueue, mango.Version.V1, True) - return mango.PerpEventQueue(account_info, mango.Version.V1, meta_data, head, count, sequence_number, unprocessed, processed) + meta_data: mango.Metadata = mango.Metadata( + mango.layouts.DATA_TYPE.EventQueue, mango.Version.V1, True + ) + return mango.PerpEventQueue( + account_info, + mango.Version.V1, + meta_data, + head, + count, + sequence_number, + unprocessed, + processed, + ) class TstPE(mango.PerpEvent): @@ -52,34 +79,77 @@ class TstPE(mango.PerpEvent): class TstFillPE(mango.PerpFillEvent): - def __init__(self, maker: PublicKey, maker_id: int, taker: PublicKey, taker_id: int): - super().__init__(0, Decimal(0), datetime.now(), mango.Side.BUY, - Decimal(1), Decimal(1), Decimal(1), Decimal(1), True, - maker, Decimal(maker_id), Decimal(0), - taker, Decimal(taker_id), Decimal(0)) + def __init__( + self, maker: PublicKey, maker_id: int, taker: PublicKey, taker_id: int + ): + super().__init__( + 0, + Decimal(0), + datetime.now(), + mango.Side.BUY, + Decimal(1), + Decimal(1), + Decimal(1), + Decimal(1), + True, + maker, + Decimal(maker_id), + Decimal(0), + taker, + Decimal(taker_id), + Decimal(0), + ) def __str__(self) -> str: return f"ยซ TstFillPE [{self.maker_order_id} / {self.taker_order_id}] ยป" def test_unseen_with_no_changes() -> None: - initial = _fake_pev(Decimal(5), Decimal(2), Decimal(7), [], [TstPE(), TstPE(), TstPE(), TstPE(), TstPE()]) - actual: mango.UnseenPerpEventChangesTracker = mango.UnseenPerpEventChangesTracker(initial) + initial = _fake_pev( + Decimal(5), + Decimal(2), + Decimal(7), + [], + [TstPE(), TstPE(), TstPE(), TstPE(), TstPE()], + ) + actual: mango.UnseenPerpEventChangesTracker = mango.UnseenPerpEventChangesTracker( + initial + ) assert actual.last_sequence_number == Decimal(7) - updated = _fake_pev(Decimal(5), Decimal(2), Decimal(7), [], [TstPE(), TstPE(), TstPE(), TstPE(), TstPE()]) + updated = _fake_pev( + Decimal(5), + Decimal(2), + Decimal(7), + [], + [TstPE(), TstPE(), TstPE(), TstPE(), TstPE()], + ) unseen = actual.unseen(updated) assert len(unseen) == 0 assert actual.last_sequence_number == Decimal(7) def test_unseen_with_one_unprocessed_change() -> None: - initial = _fake_pev(Decimal(1), Decimal(0), Decimal(1), [TstPE()], [TstPE(), TstPE(), TstPE(), TstPE()]) - actual: mango.UnseenPerpEventChangesTracker = mango.UnseenPerpEventChangesTracker(initial) + initial = _fake_pev( + Decimal(1), + Decimal(0), + Decimal(1), + [TstPE()], + [TstPE(), TstPE(), TstPE(), TstPE()], + ) + actual: mango.UnseenPerpEventChangesTracker = mango.UnseenPerpEventChangesTracker( + initial + ) assert actual.last_sequence_number == Decimal(1) marker = TstPE(50) - updated = _fake_pev(Decimal(2), Decimal(1), Decimal(2), [marker], [TstPE(), TstPE(), TstPE(), TstPE()]) + updated = _fake_pev( + Decimal(2), + Decimal(1), + Decimal(2), + [marker], + [TstPE(), TstPE(), TstPE(), TstPE()], + ) unseen = actual.unseen(updated) assert actual.last_sequence_number == Decimal(2) assert len(unseen) == 1 @@ -87,13 +157,23 @@ def test_unseen_with_one_unprocessed_change() -> None: def test_unseen_with_two_unprocessed_changes() -> None: - initial = _fake_pev(Decimal(1), Decimal(0), Decimal(1), [], [TstPE(), TstPE(), TstPE()]) - actual: mango.UnseenPerpEventChangesTracker = mango.UnseenPerpEventChangesTracker(initial) + initial = _fake_pev( + Decimal(1), Decimal(0), Decimal(1), [], [TstPE(), TstPE(), TstPE()] + ) + actual: mango.UnseenPerpEventChangesTracker = mango.UnseenPerpEventChangesTracker( + initial + ) assert actual.last_sequence_number == Decimal(1) marker1 = TstPE(50) marker2 = TstPE(51) - updated = _fake_pev(Decimal(3), Decimal(2), Decimal(3), [marker1, marker2], [TstPE(), TstPE(), TstPE()]) + updated = _fake_pev( + Decimal(3), + Decimal(2), + Decimal(3), + [marker1, marker2], + [TstPE(), TstPE(), TstPE()], + ) unseen = actual.unseen(updated) assert actual.last_sequence_number == Decimal(3) assert len(unseen) == 2 @@ -104,13 +184,23 @@ def test_unseen_with_two_unprocessed_changes() -> None: def test_unseen_with_two_processed_changes() -> None: # This should be identical to the previous test - it shouldn't matter to 'seen' tracking whether an event # is processed or not. - initial = _fake_pev(Decimal(1), Decimal(0), Decimal(1), [], [TstPE(), TstPE(), TstPE()]) - actual: mango.UnseenPerpEventChangesTracker = mango.UnseenPerpEventChangesTracker(initial) + initial = _fake_pev( + Decimal(1), Decimal(0), Decimal(1), [], [TstPE(), TstPE(), TstPE()] + ) + actual: mango.UnseenPerpEventChangesTracker = mango.UnseenPerpEventChangesTracker( + initial + ) assert actual.last_sequence_number == Decimal(1) marker1 = TstPE(50) marker2 = TstPE(51) - updated = _fake_pev(Decimal(3), Decimal(0), Decimal(3), [marker1, marker2], [TstPE(), TstPE(), TstPE()]) + updated = _fake_pev( + Decimal(3), + Decimal(0), + Decimal(3), + [marker1, marker2], + [TstPE(), TstPE(), TstPE()], + ) unseen = actual.unseen(updated) assert actual.last_sequence_number == Decimal(3) assert len(unseen) == 2 @@ -123,13 +213,23 @@ def test_unseen_with_two_unprocessed_changes_wrapping_around() -> None: # to the next slot (which is the last slot in the array) and then another is added to the next slot # (which is the first slot in the array). Seen tracking shouldn't care - it should just return the # unseen events in the proper order. - initial = _fake_pev(Decimal(4), Decimal(0), Decimal(7), [], [TstPE(), TstPE(), TstPE()]) - actual: mango.UnseenPerpEventChangesTracker = mango.UnseenPerpEventChangesTracker(initial) + initial = _fake_pev( + Decimal(4), Decimal(0), Decimal(7), [], [TstPE(), TstPE(), TstPE()] + ) + actual: mango.UnseenPerpEventChangesTracker = mango.UnseenPerpEventChangesTracker( + initial + ) assert actual.last_sequence_number == Decimal(7) marker1 = TstPE(50) marker2 = TstPE(51) - updated = _fake_pev(Decimal(1), Decimal(2), Decimal(9), [marker1, marker2], [TstPE(), TstPE(), TstPE()]) + updated = _fake_pev( + Decimal(1), + Decimal(2), + Decimal(9), + [marker1, marker2], + [TstPE(), TstPE(), TstPE()], + ) unseen = actual.unseen(updated) assert actual.last_sequence_number == Decimal(9) assert len(unseen) == 2 @@ -180,7 +280,13 @@ def test_unseen_fills_for_account() -> None: order4 = TstFillPE(user3, 14, user2, 24) order5 = TstFillPE(user1, 15, user3, 25) order6 = TstFillPE(user2, 16, user1, 26) - pev2 = _fake_pev(Decimal(4), Decimal(0), Decimal(7), [], [order1, order2, order3, order4, order5, order6]) + pev2 = _fake_pev( + Decimal(4), + Decimal(0), + Decimal(7), + [], + [order1, order2, order3, order4, order5, order6], + ) my_unseen_fills = actual.unseen(pev2) assert len(my_unseen_fills) == 2 @@ -204,7 +310,13 @@ def test_no_unseen_fills_for_account() -> None: order4 = TstFillPE(user3, 14, user2, 24) order5 = TstFillPE(user1, 15, user3, 25) order6 = TstFillPE(user2, 16, user1, 26) - pev2 = _fake_pev(Decimal(4), Decimal(0), Decimal(7), [], [order1, order2, order3, order4, order5, order6]) + pev2 = _fake_pev( + Decimal(4), + Decimal(0), + Decimal(7), + [], + [order1, order2, order3, order4, order5, order6], + ) my_unseen_fills = actual.unseen(pev2) assert len(my_unseen_fills) == 0 @@ -225,7 +337,13 @@ def test_no_changes_in_unseen_fills_for_account() -> None: order4 = TstFillPE(user3, 14, user2, 24) order5 = TstFillPE(user4, 15, user3, 25) order6 = TstFillPE(user2, 16, user4, 26) - pev2 = _fake_pev(Decimal(4), Decimal(0), Decimal(7), [], [order1, order2, order3, order4, order5, order6]) + pev2 = _fake_pev( + Decimal(4), + Decimal(0), + Decimal(7), + [], + [order1, order2, order3, order4, order5, order6], + ) my_unseen_fills = actual.unseen(pev2) assert len(my_unseen_fills) == 0 diff --git a/tests/test_publickey.py b/tests/test_publickey.py index 54f8bbf..18fe1d9 100644 --- a/tests/test_publickey.py +++ b/tests/test_publickey.py @@ -126,7 +126,7 @@ def test_publickey_sorting() -> None: PublicKey("2jXpCvzJkDxUSxpP3U7VUGSDCDpWDZqZGg2LGsGrhUDp"), PublicKey("7wDMKabpaBC1kj41hmotn1qBQhKSqRpGvAb1x347m6zT"), PublicKey("DcRaC7n2ws825JGXqf4cRr8mbXWXf9k5e2xDbFYC1EH7"), - PublicKey("E127URgoUVjVmuvwLEbvtEdiSkrsMjBL8J6zrV1djN7a") + PublicKey("E127URgoUVjVmuvwLEbvtEdiSkrsMjBL8J6zrV1djN7a"), ] expected = [ @@ -229,13 +229,15 @@ def test_publickey_sorting() -> None: PublicKey("HNEq27KbPirQg2p4hxWGTqV5SVHZ6hgWFPztgiPsR6hn"), PublicKey("Hgbt3PYF3CPjJxwgurNPtU7PxKWZawxbVYAY3PHuqcRY"), PublicKey("J9DXnDPgxpvyGfXULryE8GXWe7DawwbcrJGrGqfEtLa1"), - PublicKey("JCxwtTLkh83ArtcnZtT4KAKB774MvnYTLpLj9sR751rb") + PublicKey("JCxwtTLkh83ArtcnZtT4KAKB774MvnYTLpLj9sR751rb"), ] test_keys.sort(key=mango.encode_public_key_for_sorting) for counter in range(len(test_keys)): - assert test_keys[counter] == expected[counter], f"Index {counter} - {test_keys[counter]} does not match expected {expected[counter]}" + assert ( + test_keys[counter] == expected[counter] + ), f"Index {counter} - {test_keys[counter]} does not match expected {expected[counter]}" # This is the same test but with results from sorting in a BPF Solana runtime. @@ -257,7 +259,7 @@ def test_publickey_bpf_sorting() -> None: PublicKey("789UPSUbj9TYSm12e66qo8PCE7RvDygqFKyy7qvCjtBD"), PublicKey("Hgbt3PYF3CPjJxwgurNPtU7PxKWZawxbVYAY3PHuqcRY"), PublicKey("jD2gcCANgvQM54brip9jT9L3PHfG3YmoBALQeWQ2QFt"), - PublicKey("8ZuYuQdGesgcKs6UaiHZNo44iijnsFJ4zUEF3KymUhjw") + PublicKey("8ZuYuQdGesgcKs6UaiHZNo44iijnsFJ4zUEF3KymUhjw"), ] expected = [ @@ -274,13 +276,15 @@ def test_publickey_bpf_sorting() -> None: PublicKey("FyjuBBN5fUHjtpB5LbSVW1mMocWCBebWJEecr1YN1TaQ"), PublicKey("GFQFVSEYN9ho4UwA4KJefTGrrnV8xKwyT2U5VoY4ABRw"), PublicKey("HMbNVVb6uqqhuTaQU4RmKDeG3VZ1pvRn3PgnhfevbjWJ"), - PublicKey("Hgbt3PYF3CPjJxwgurNPtU7PxKWZawxbVYAY3PHuqcRY") + PublicKey("Hgbt3PYF3CPjJxwgurNPtU7PxKWZawxbVYAY3PHuqcRY"), ] test_keys.sort(key=mango.encode_public_key_for_sorting) for counter in range(len(test_keys)): - assert test_keys[counter] == expected[counter], f"Index {counter} - {test_keys[counter]} does not match expected {expected[counter]}" + assert ( + test_keys[counter] == expected[counter] + ), f"Index {counter} - {test_keys[counter]} does not match expected {expected[counter]}" # This is a short test to help with debugging, with results from the BPF Solana runtime. @@ -299,10 +303,12 @@ def test_publickey_short_sorting() -> None: PublicKey("AuAYgwDerZryPif7Zw1ZqACYgJFRqmKwy3ZqASr2Wu7d"), PublicKey("CpFj2d5uYjeh34FKh6iYRTE2dL3N9NaSrZtyZVZ7eQwa"), PublicKey("FFzYWt9K2ZDeyxpDbvKbma6232baDHoCFGL1gZAKiox1"), - PublicKey("HMbNVVb6uqqhuTaQU4RmKDeG3VZ1pvRn3PgnhfevbjWJ") + PublicKey("HMbNVVb6uqqhuTaQU4RmKDeG3VZ1pvRn3PgnhfevbjWJ"), ] test_keys.sort(key=mango.encode_public_key_for_sorting) for counter in range(len(test_keys)): - assert test_keys[counter] == expected[counter], f"Index {counter} - {test_keys[counter]} does not match expected {expected[counter]}" + assert ( + test_keys[counter] == expected[counter] + ), f"Index {counter} - {test_keys[counter]} does not match expected {expected[counter]}" diff --git a/tests/test_spotmarket.py b/tests/test_spotmarket.py index 4fd6830..3fbe080 100644 --- a/tests/test_spotmarket.py +++ b/tests/test_spotmarket.py @@ -7,8 +7,12 @@ from decimal import Decimal def test_spot_market_stub_constructor() -> None: program_address = fake_seeded_public_key("program address") address = fake_seeded_public_key("spot market address") - base = mango.Token("BASE", "Base Token", Decimal(7), fake_seeded_public_key("base token")) - quote = mango.Token("QUOTE", "Quote Token", Decimal(9), fake_seeded_public_key("quote token")) + base = mango.Token( + "BASE", "Base Token", Decimal(7), fake_seeded_public_key("base token") + ) + quote = mango.Token( + "QUOTE", "Quote Token", Decimal(9), fake_seeded_public_key("quote token") + ) group_address = fake_seeded_public_key("group address") actual = mango.SpotMarketStub(program_address, address, base, quote, group_address) assert actual is not None diff --git a/tests/test_tokenaccount.py b/tests/test_tokenaccount.py index 49b8273..53ed1ac 100644 --- a/tests/test_tokenaccount.py +++ b/tests/test_tokenaccount.py @@ -8,5 +8,7 @@ def test_constructor() -> None: token = fake_token() token_value = mango.InstrumentValue(token, Decimal(6)) owner = fake_seeded_public_key("token owner") - actual = mango.TokenAccount(fake_account_info(), mango.Version.V1, owner, token_value) + actual = mango.TokenAccount( + fake_account_info(), mango.Version.V1, owner, token_value + ) assert actual is not None diff --git a/tests/test_tokenbank.py b/tests/test_tokenbank.py index ca22b43..6287ee9 100644 --- a/tests/test_tokenbank.py +++ b/tests/test_tokenbank.py @@ -9,7 +9,9 @@ from solana.publickey import PublicKey def test_node_bank_constructor() -> None: account_info = fake_account_info(fake_seeded_public_key("node bank")) - meta_data = mango.Metadata(mango.layouts.DATA_TYPE.parse(bytearray(b'\x03')), mango.Version.V1, True) + meta_data = mango.Metadata( + mango.layouts.DATA_TYPE.parse(bytearray(b"\x03")), mango.Version.V1, True + ) deposits = Decimal(1000) borrows = Decimal(100) balances = mango.BankBalances(deposits=deposits, borrows=borrows) @@ -28,7 +30,9 @@ def test_node_bank_constructor() -> None: def test_root_bank_constructor() -> None: account_info = fake_account_info(fake_seeded_public_key("root bank")) - meta_data = mango.Metadata(mango.layouts.DATA_TYPE.parse(bytearray(b'\x02')), mango.Version.V1, True) + meta_data = mango.Metadata( + mango.layouts.DATA_TYPE.parse(bytearray(b"\x02")), mango.Version.V1, True + ) optimal_util = Decimal("0.7") optimal_rate = Decimal("0.06") max_rate = Decimal("1.5") @@ -37,8 +41,18 @@ def test_root_bank_constructor() -> None: borrow_index = Decimal(12345) timestamp = datetime.now() - actual = mango.RootBank(account_info, mango.Version.V1, meta_data, optimal_util, optimal_rate, - max_rate, [node_bank], deposit_index, borrow_index, timestamp) + actual = mango.RootBank( + account_info, + mango.Version.V1, + meta_data, + optimal_util, + optimal_rate, + max_rate, + [node_bank], + deposit_index, + borrow_index, + timestamp, + ) assert actual is not None assert actual.account_info == account_info assert actual.address == fake_seeded_public_key("root bank") @@ -74,15 +88,21 @@ def test_load_root_bank() -> None: assert actual.optimal_util == Decimal("0.69999999999999928946") assert actual.optimal_rate == Decimal("0.05999999999999872102") assert actual.max_rate == Decimal("1.5") - assert actual.node_banks[0] == PublicKey("J2Lmnc1e4frMnBEJARPoHtfpcohLfN67HdK1inXjTFSM") + assert actual.node_banks[0] == PublicKey( + "J2Lmnc1e4frMnBEJARPoHtfpcohLfN67HdK1inXjTFSM" + ) assert actual.deposit_index == Decimal("1000154.42276607355830719825") assert actual.borrow_index == Decimal("1000219.00867863010088498754") assert actual.last_updated == datetime(2021, 10, 4, 14, 58, 5, 0, timezone.utc) def test_btc_token_bank() -> None: - btc = mango.Token("BTC", "Wrapped Bitcoin (Sollet)", Decimal( - 6), PublicKey("9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E")) + btc = mango.Token( + "BTC", + "Wrapped Bitcoin (Sollet)", + Decimal(6), + PublicKey("9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E"), + ) root_bank = load_root_bank("tests/testdata/tokenbank/btc_root_bank.json") node_bank = load_node_bank("tests/testdata/tokenbank/btc_node_bank.json") @@ -95,15 +115,21 @@ def test_btc_token_bank() -> None: interest_rates = actual.fetch_interest_rates(context) # Typescript says: 0.00074328994922723268 - assert interest_rates.deposit == Decimal("0.000743289949230430278650314704786385301") + assert interest_rates.deposit == Decimal( + "0.000743289949230430278650314704786385301" + ) # Typescript says: 0.0060962691428017024 assert interest_rates.borrow == Decimal("0.00609626914280543412386251743252599320") assert str(interest_rates) == "ยซ InterestRates Deposit: 0.07% Borrow: 0.61% ยป" def test_usdc_token_bank() -> None: - usdc = mango.Token("USDC", "USD Coin", Decimal( - 6), PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")) + usdc = mango.Token( + "USDC", + "USD Coin", + Decimal(6), + PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"), + ) root_bank = load_root_bank("tests/testdata/tokenbank/usdc_root_bank.json") node_bank = load_node_bank("tests/testdata/tokenbank/usdc_node_bank.json") diff --git a/tests/test_tradeexecutor.py b/tests/test_tradeexecutor.py index 229b33a..691ef76 100644 --- a/tests/test_tradeexecutor.py +++ b/tests/test_tradeexecutor.py @@ -19,6 +19,7 @@ def test_trade_executor_constructor() -> None: def test_null_trade_executor_constructor() -> None: def reporter(x: typing.Any) -> None: return None + actual = mango.NullTradeExecutor(reporter) assert actual is not None assert actual.reporter == reporter @@ -31,7 +32,10 @@ def test_serum_trade_executor_constructor() -> None: def reporter(x: typing.Any) -> None: return None - actual = mango.ImmediateTradeExecutor(context, wallet, None, price_adjustment_factor, reporter) + + actual = mango.ImmediateTradeExecutor( + context, wallet, None, price_adjustment_factor, reporter + ) assert actual is not None assert actual.context == context assert actual.wallet == wallet diff --git a/tests/test_transactionscout.py b/tests/test_transactionscout.py index 1bf2776..b3cb00b 100644 --- a/tests/test_transactionscout.py +++ b/tests/test_transactionscout.py @@ -37,10 +37,23 @@ def test_transaction_scout_constructor() -> None: token_value = mango.InstrumentValue(token, Decimal(28)) owner = fake_seeded_public_key("owner") owned_token_value = mango.OwnedInstrumentValue(owner, token_value) - pre_token_balances: typing.Sequence[mango.OwnedInstrumentValue] = [owned_token_value] - post_token_balances: typing.Sequence[mango.OwnedInstrumentValue] = [owned_token_value] - actual = mango.TransactionScout(timestamp, signatures, succeeded, group_name, accounts, - instructions, messages, pre_token_balances, post_token_balances) + pre_token_balances: typing.Sequence[mango.OwnedInstrumentValue] = [ + owned_token_value + ] + post_token_balances: typing.Sequence[mango.OwnedInstrumentValue] = [ + owned_token_value + ] + actual = mango.TransactionScout( + timestamp, + signatures, + succeeded, + group_name, + accounts, + instructions, + messages, + pre_token_balances, + post_token_balances, + ) assert actual is not None assert actual.timestamp == timestamp assert actual.signatures == signatures diff --git a/tests/test_walletbalancer.py b/tests/test_walletbalancer.py index afa9ae0..6a8e24c 100644 --- a/tests/test_walletbalancer.py +++ b/tests/test_walletbalancer.py @@ -5,11 +5,24 @@ from decimal import Decimal from solana.publickey import PublicKey -ETH_TOKEN = mango.Token("ETH", "Wrapped Ethereum (Sollet)", Decimal( - 6), PublicKey("2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk")) -BTC_TOKEN = mango.Token("BTC", "Wrapped Bitcoin (Sollet)", Decimal( - 6), PublicKey("9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E")) -USDT_TOKEN = mango.Token("USDT", "USDT", Decimal(6), PublicKey("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB")) +ETH_TOKEN = mango.Token( + "ETH", + "Wrapped Ethereum (Sollet)", + Decimal(6), + PublicKey("2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk"), +) +BTC_TOKEN = mango.Token( + "BTC", + "Wrapped Bitcoin (Sollet)", + Decimal(6), + PublicKey("9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E"), +) +USDT_TOKEN = mango.Token( + "USDT", + "USDT", + Decimal(6), + PublicKey("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"), +) def test_target_balance_constructor() -> None: @@ -37,7 +50,9 @@ def test_percentage_target_balance_constructor() -> None: actual = mango.PercentageTargetBalance(token.symbol, value) assert actual is not None assert actual.symbol == token.symbol - assert actual.target_fraction == Decimal("0.05") # Calculated as a fraction instead of a percentage. + assert actual.target_fraction == Decimal( + "0.05" + ) # Calculated as a fraction instead of a percentage. def test_calculate_required_balance_changes() -> None: @@ -48,44 +63,54 @@ def test_calculate_required_balance_changes() -> None: ] desired_balances = [ mango.InstrumentValue(ETH_TOKEN, Decimal("1")), - mango.InstrumentValue(BTC_TOKEN, Decimal("0.1")) + mango.InstrumentValue(BTC_TOKEN, Decimal("0.1")), ] - changes = mango.calculate_required_balance_changes(current_balances, desired_balances) + changes = mango.calculate_required_balance_changes( + current_balances, desired_balances + ) - assert(changes[0].token.symbol == "ETH") - assert(changes[0].value == Decimal("0.5")) - assert(changes[1].token.symbol == "BTC") - assert(changes[1].value == Decimal("-0.1")) + assert changes[0].token.symbol == "ETH" + assert changes[0].value == Decimal("0.5") + assert changes[1].token.symbol == "BTC" + assert changes[1].value == Decimal("-0.1") def test_percentage_target_balance() -> None: token = fake_token() - percentage_parsed_balance_change = mango.PercentageTargetBalance(token.symbol, Decimal(33)) - assert(percentage_parsed_balance_change.symbol == token.symbol) + percentage_parsed_balance_change = mango.PercentageTargetBalance( + token.symbol, Decimal(33) + ) + assert percentage_parsed_balance_change.symbol == token.symbol current_token_price = Decimal(2000) # It's $2,000 per TOKEN - current_account_value = Decimal(10000) # We hold $10,000 in total across all assets in our account. - resolved_parsed_balance_change = percentage_parsed_balance_change.resolve(token, - current_token_price, - current_account_value) - assert(resolved_parsed_balance_change.token == token) + current_account_value = Decimal( + 10000 + ) # We hold $10,000 in total across all assets in our account. + resolved_parsed_balance_change = percentage_parsed_balance_change.resolve( + token, current_token_price, current_account_value + ) + assert resolved_parsed_balance_change.token == token # 33% of $10,000 is $3,300 # $3,300 spent on TOKEN gives us 1.65 TOKEN - assert(resolved_parsed_balance_change.value == Decimal("1.65")) + assert resolved_parsed_balance_change.value == Decimal("1.65") def test_target_balance_parser_fixedvalue() -> None: parsed = mango.parse_target_balance("eth:70") assert isinstance(parsed, mango.FixedTargetBalance) - assert parsed.symbol == "eth" # Case is preserved but comparisons should be case-insensitive + assert ( + parsed.symbol == "eth" + ) # Case is preserved but comparisons should be case-insensitive assert parsed.value == Decimal(70) def test_target_balance_parser_percentagevalue() -> None: parsed = mango.parse_target_balance("btc:10%") assert isinstance(parsed, mango.PercentageTargetBalance) - assert parsed.symbol == "btc" # Case is preserved but comparisons should be case-insensitive + assert ( + parsed.symbol == "btc" + ) # Case is preserved but comparisons should be case-insensitive assert parsed.target_fraction == Decimal("0.1") @@ -100,15 +125,21 @@ def test_filter_small_changes_constructor() -> None: mango.InstrumentValue(BTC_TOKEN, Decimal("0.2")), mango.InstrumentValue(USDT_TOKEN, Decimal("10000")), ] - action_threshold = Decimal("0.01") # Don't bother if it's less than 1% of the total value (24,000) + action_threshold = Decimal( + "0.01" + ) # Don't bother if it's less than 1% of the total value (24,000) expected_prices = { current_prices[0].token.symbol: current_prices[0], current_prices[1].token.symbol: current_prices[1], - current_prices[2].token.symbol: current_prices[2] + current_prices[2].token.symbol: current_prices[2], } expected_total_balance = Decimal(24000) - expected_action_threshold_value = expected_total_balance / 100 # Action threshold is 0.01 - actual = mango.FilterSmallChanges(action_threshold, current_balances, current_prices) + expected_action_threshold_value = ( + expected_total_balance / 100 + ) # Action threshold is 0.01 + actual = mango.FilterSmallChanges( + action_threshold, current_balances, current_prices + ) assert actual is not None assert actual.prices == expected_prices assert actual.total_balance == expected_total_balance @@ -126,23 +157,24 @@ def test_filtering_small_changes() -> None: mango.InstrumentValue(BTC_TOKEN, Decimal("0.2")), mango.InstrumentValue(USDT_TOKEN, Decimal("10000")), ] - action_threshold = Decimal("0.01") # Don't bother if it's less than 1% of the total value (24,000) - actual = mango.FilterSmallChanges(action_threshold, current_balances, current_prices) + action_threshold = Decimal( + "0.01" + ) # Don't bother if it's less than 1% of the total value (24,000) + actual = mango.FilterSmallChanges( + action_threshold, current_balances, current_prices + ) # 0.05 ETH is worth $200 at our test prices, which is less than our $240 threshold - assert(not actual.allow(mango.InstrumentValue(ETH_TOKEN, Decimal("0.05")))) + assert not actual.allow(mango.InstrumentValue(ETH_TOKEN, Decimal("0.05"))) # 0.05 BTC is worth $3,000 at our test prices, which is much more than our $240 threshold - assert(actual.allow(mango.InstrumentValue(BTC_TOKEN, Decimal("0.05")))) + assert actual.allow(mango.InstrumentValue(BTC_TOKEN, Decimal("0.05"))) def test_sort_changes_for_trades() -> None: eth_buy = mango.InstrumentValue(ETH_TOKEN, Decimal("5")) btc_sell = mango.InstrumentValue(BTC_TOKEN, Decimal("-1")) - sorted_changes = mango.sort_changes_for_trades([ - eth_buy, - btc_sell - ]) + sorted_changes = mango.sort_changes_for_trades([eth_buy, btc_sell]) - assert(sorted_changes[0] == btc_sell) - assert(sorted_changes[1] == eth_buy) + assert sorted_changes[0] == btc_sell + assert sorted_changes[1] == eth_buy