mango-explorer/AccountLiquidator.ipynb

307 lines
12 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"cells": [
{
"cell_type": "markdown",
"id": "qualified-traveler",
"metadata": {},
"source": [
"# ⚠ Warning\n",
"\n",
"THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n",
"\n",
"[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gl/OpinionatedGeek%2Fmango-explorer/HEAD?filepath=AccountLiquidator.ipynb) _🏃 To run this notebook press the ⏩ icon in the toolbar above._\n",
"\n",
"[🥭 Mango Markets](https://mango.markets/) support is available at: [Docs](https://docs.mango.markets/) | [Discord](https://discord.gg/67jySBhxrg) | [Twitter](https://twitter.com/mangomarkets) | [Github](https://github.com/blockworks-foundation) | [Email](mailto:hello@blockworks.foundation)"
]
},
{
"cell_type": "markdown",
"id": "rough-evening",
"metadata": {},
"source": [
"# 🥭 AccountLiquidator\n",
"\n",
"An `AccountLiquidator` liquidates a `MarginAccount`, if possible.\n",
"\n",
"The follows the common pattern of having an abstract base class that defines the interface external code should use, along with a 'null' implementation and at least one full implementation.\n",
"\n",
"The idea is that preparing code can choose whether to use the null implementation (in the case of a 'dry run' for instance) or the full implementation, but the code that defines the algorithm - which actually calls the `AccountLiquidator` - doesn't have to care about this choice."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "wanted-charger",
"metadata": {
"jupyter": {
"source_hidden": true
}
},
"outputs": [],
"source": [
"import abc\n",
"import logging\n",
"import typing\n",
"\n",
"from solana.transaction import Transaction\n",
"\n",
"from BaseModel import Group, MarginAccount, TokenValue\n",
"from Context import Context\n",
"from Instructions import ForceCancelOrdersInstructionBuilder, InstructionBuilder, LiquidateInstructionBuilder\n",
"from Wallet import Wallet\n"
]
},
{
"cell_type": "markdown",
"id": "confidential-helmet",
"metadata": {},
"source": [
"## 💧 AccountLiquidator class\n",
"\n",
"This abstract base class defines the interface to account liquidators, which in this case is just the `liquidate()` method.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "promotional-andrews",
"metadata": {},
"outputs": [],
"source": [
"class AccountLiquidator(metaclass=abc.ABCMeta):\n",
" def __init__(self):\n",
" self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)\n",
"\n",
" @abc.abstractmethod\n",
" def liquidate(self, group: Group, margin_account: MarginAccount, prices: typing.List[TokenValue]) -> typing.Optional[str]:\n",
" raise NotImplementedError(\"AccountLiquidator.liquidate() is not implemented on the base type.\")\n"
]
},
{
"cell_type": "markdown",
"id": "statewide-miniature",
"metadata": {},
"source": [
"## 🌬️ NullAccountLiquidator class\n",
"\n",
"A 'null', 'no-op', 'dry run' implementation of the `AccountLiquidator` class."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "romantic-faculty",
"metadata": {},
"outputs": [],
"source": [
"class NullAccountLiquidator(AccountLiquidator):\n",
" def __init__(self):\n",
" super().__init__()\n",
"\n",
" def liquidate(self, group: Group, margin_account: MarginAccount, prices: typing.List[TokenValue]) -> typing.Optional[str]:\n",
" self.logger.info(f\"Skipping liquidation of margin account [{margin_account.address}]\")\n",
" return None\n"
]
},
{
"cell_type": "markdown",
"id": "awful-shift",
"metadata": {},
"source": [
"## 💧 ActualAccountLiquidator class\n",
"\n",
"This full implementation takes a `MarginAccount` and liquidates it.\n",
"\n",
"It can also serve as a base class for further derivation. Derived classes may override `prepare_instructions()` to extend the liquidation process (for example to cancel outstanding orders before liquidating)."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "experimental-genetics",
"metadata": {},
"outputs": [],
"source": [
"class ActualAccountLiquidator:\n",
" def __init__(self, context: Context, wallet: Wallet):\n",
" super().__init__()\n",
" self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)\n",
" self.context = context\n",
" self.wallet = wallet\n",
"\n",
" def prepare_instructions(self, group: Group, margin_account: MarginAccount, prices: typing.List[TokenValue]) -> typing.List[InstructionBuilder]:\n",
" liquidate_instructions: typing.List[InstructionBuilder] = []\n",
" liquidate_instruction = LiquidateInstructionBuilder.from_margin_account_and_market(self.context, group, self.wallet, margin_account, prices)\n",
" if liquidate_instruction is not None:\n",
" liquidate_instructions += [liquidate_instruction]\n",
"\n",
" return liquidate_instructions\n",
"\n",
" def liquidate(self, group: Group, margin_account: MarginAccount, prices: typing.List[TokenValue]) -> typing.Optional[str]:\n",
" instructions = self.prepare_instructions(group, margin_account, prices)\n",
"\n",
" if len(instructions) == 0:\n",
" return None\n",
"\n",
" transaction = Transaction()\n",
" for instruction in instructions:\n",
" transaction.add(instruction.build())\n",
"\n",
" for instruction in transaction.instructions:\n",
" self.logger.debug(\"INSTRUCTION\")\n",
" self.logger.debug(\" Keys:\")\n",
" for key in instruction.keys:\n",
" self.logger.debug(\" \", f\"{key.pubkey}\".ljust(45), f\"{key.is_signer}\".ljust(6), f\"{key.is_writable}\".ljust(6))\n",
" self.logger.debug(\" Data:\", \" \".join(f\"{x:02x}\" for x in instruction.data))\n",
" self.logger.debug(\" Program ID:\", instruction.program_id)\n",
"\n",
" transaction_response = self.context.client.send_transaction(transaction, self.wallet.account)\n",
" transaction_id = self.context.unwrap_transaction_id_or_raise_exception(transaction_response)\n",
" return transaction_id\n",
"\n"
]
},
{
"cell_type": "markdown",
"id": "forward-premises",
"metadata": {},
"source": [
"# 🌪️ ForceCancelOrdersAccountLiquidator class\n",
"\n",
"When liquidating an account, it's a good idea to ensure it has no open orders that could lock funds. This is why Mango allows a liquidator to force-close orders on a liquidatable account.\n",
"\n",
"`ForceCancelOrdersAccountLiquidator` overrides `prepare_instructions()` to inject any necessary force-cancel instructions before the `PartialLiquidate` instruction.\n",
"\n",
"This is not always necessary. For example, if the liquidator is partially-liquidating a large account, then perhaps only the first partial-liquidate needs to check and force-close orders, and subsequent partial liquidations can skip this step as an optimisation.\n",
"\n",
"The separation of the regular `AccountLiquidator` and the `ForceCancelOrdersAccountLiquidator` classes allows the caller to determine which process is used."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "fantastic-behalf",
"metadata": {},
"outputs": [],
"source": [
"class ForceCancelOrdersAccountLiquidator(ActualAccountLiquidator):\n",
" def __init__(self, context: Context, wallet: Wallet):\n",
" super().__init__(context, wallet)\n",
"\n",
" def prepare_instructions(self, group: Group, margin_account: MarginAccount, prices: typing.List[TokenValue]) -> typing.List[InstructionBuilder]:\n",
" force_cancel_orders_instructions: typing.List[InstructionBuilder] = []\n",
" for index, market_metadata in enumerate(group.markets):\n",
" open_orders = margin_account.open_orders_accounts[index]\n",
" if open_orders is not None:\n",
" market = market_metadata.fetch_market(self.context)\n",
" orders = market.load_orders_for_owner(margin_account.owner)\n",
" order_count = len(orders)\n",
" if order_count > 0:\n",
" force_cancel_orders_instructions += ForceCancelOrdersInstructionBuilder.multiple_instructions_from_margin_account_and_market(self.context, group, self.wallet, margin_account, market_metadata, order_count)\n",
"\n",
" all_instructions = force_cancel_orders_instructions + super().prepare_instructions(group, margin_account, prices)\n",
"\n",
" return all_instructions\n"
]
},
{
"cell_type": "markdown",
"id": "emotional-catering",
"metadata": {},
"source": [
"# 🏃 Running"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "dangerous-ivory",
"metadata": {},
"outputs": [],
"source": [
"if __name__ == \"__main__\":\n",
" logging.getLogger().setLevel(logging.INFO)\n",
"\n",
" from Context import default_context\n",
" from Wallet import default_wallet\n",
"\n",
" if default_wallet is None:\n",
" print(\"No default wallet file available.\")\n",
" else:\n",
" group = Group.load(default_context)\n",
" prices = group.fetch_token_prices()\n",
" margin_accounts = MarginAccount.load_all_for_owner(default_context, default_wallet.address, group)\n",
" for margin_account in margin_accounts:\n",
" account_liquidator = ActualAccountLiquidator(default_context, default_wallet)\n",
" print(account_liquidator.prepare_instructions(group, margin_account, prices))\n",
"\n",
" force_cancel_orders_account_liquidator = ForceCancelOrdersAccountLiquidator(default_context, default_wallet)\n",
" print(force_cancel_orders_account_liquidator.prepare_instructions(group, margin_account, prices))\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.6"
},
"toc": {
"base_numbering": 1,
"nav_menu": {},
"number_sections": true,
"sideBar": true,
"skip_h1_title": false,
"title_cell": "Table of Contents",
"title_sidebar": "Contents",
"toc_cell": false,
"toc_position": {},
"toc_section_display": true,
"toc_window_display": true
},
"varInspector": {
"cols": {
"lenName": 16,
"lenType": 16,
"lenVar": 40
},
"kernels_config": {
"python": {
"delete_cmd_postfix": "",
"delete_cmd_prefix": "del ",
"library": "var_list.py",
"varRefreshCmd": "print(var_dic_list())"
},
"r": {
"delete_cmd_postfix": ") ",
"delete_cmd_prefix": "rm(",
"library": "var_list.r",
"varRefreshCmd": "cat(var_dic_list()) "
}
},
"types_to_exclude": [
"module",
"function",
"builtin_function_or_method",
"instance",
"_Feature"
],
"window_display": false
}
},
"nbformat": 4,
"nbformat_minor": 5
}