mango-explorer/SimpleLiquidator.ipynb

210 lines
9.9 KiB
Plaintext
Raw Normal View History

{
"cells": [
{
"cell_type": "markdown",
"id": "restricted-genius",
"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=SimpleLiquidator.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": "coordinate-identification",
"metadata": {},
"source": [
"# 🥭 SimpleLiquidator\n",
"\n",
"This notebook implements a simple approach to a liquidator. It loops, processes all possible liquidations, then sleeps."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "fundamental-sleeve",
"metadata": {
"jupyter": {
"source_hidden": true
}
},
"outputs": [],
"source": [
"import logging\n",
"import typing\n",
"\n",
"from AccountLiquidator import AccountLiquidator\n",
"from BaseModel import Group, MarginAccount, MarginAccountMetadata, OpenOrders, TokenValue\n",
"from Context import Context\n",
"from Wallet import Wallet\n",
"from WalletBalancer import WalletBalancer\n"
]
},
{
"cell_type": "markdown",
"id": "treated-painting",
"metadata": {},
"source": [
"# SimpleLiquidator class\n",
"\n",
"In [Liquidation](Liquidation.ipynb) it says these are probably roughly the steps to run a liquidator:\n",
"\n",
"1. Find all liquidatable margin accounts.\n",
"2. Pick the most appropriate of these margin accounts, based on that account's collatoralisation and the liquidator's token balances.\n",
"3. Pick the market with the most value in the margin account's openorders accounts.\n",
"4. Force cancellation of all outstanding orders for the margin account in that market.\n",
"5. Pick the market with the highest borrows and lowest deposits for the account being liquidated.\n",
"6. Build and send the PartialLiquidate instruction.\n",
"7. Convert the received tokens to your desired tokens.\n",
"8. Repeat from step 2 (if necessary) with fresh tokens.\n",
"\n",
"The `SimpleLiquidator` class performs steps 1, 2, 6, and 8. Steps 3, 4, and 5 are handled implicitly by the `AccountLiquidator` (in our case the `ForceCancelOrdersAccountLiquidator`). Step 7 is handled by the `Balancer`."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "innovative-escape",
"metadata": {},
"outputs": [],
"source": [
"class SimpleLiquidator:\n",
" def __init__(self, context: Context, wallet: Wallet, account_liquidator: AccountLiquidator, wallet_balancer: WalletBalancer, worthwhile_threshold: float = 0.01):\n",
" self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)\n",
" self.context = context\n",
" self.wallet = wallet\n",
" self.account_liquidator: AccountLiquidator = account_liquidator\n",
" self.wallet_balancer = wallet_balancer\n",
" self.worthwhile_threshold = worthwhile_threshold\n",
"\n",
" def run(self):\n",
" def _token_balances_from_wallet(context: Context, wallet: Wallet, group: Group) -> typing.List[TokenValue]:\n",
" balances: typing.List[TokenValue] = []\n",
" for token in group.tokens:\n",
" balance = TokenValue(token, context.fetch_token_balance(wallet.address, token.mint))\n",
" balances += [balance]\n",
"\n",
" return balances\n",
"\n",
" self.logger.info(\"Fetching all margin accounts...\")\n",
" group = Group.load(self.context)\n",
" prices = group.fetch_token_prices()\n",
" margin_accounts = MarginAccount.load_all_for_group(self.context, self.context.program_id, group)\n",
" open_orders = OpenOrders.load_raw_open_orders_accounts(self.context, group)\n",
" open_orders_by_address = {key: value for key, value in [(str(address), open_orders_account) for address, open_orders_account in open_orders]}\n",
" for margin_account in margin_accounts:\n",
" margin_account.install_open_orders_accounts(group, open_orders_by_address)\n",
" self.logger.info(f\"Fetched {len(margin_accounts)} margin accounts to process.\")\n",
"\n",
" nonzero: typing.List[MarginAccountMetadata] = []\n",
" for margin_account in margin_accounts:\n",
" balance_sheet = margin_account.get_balance_sheet_totals(group, prices)\n",
" if balance_sheet.collateral_ratio > 0:\n",
" balances = margin_account.get_intrinsic_balances(group)\n",
" nonzero += [MarginAccountMetadata(margin_account, balance_sheet, balances)]\n",
" self.logger.info(f\"Of those {len(margin_accounts)}, {len(nonzero)} have a nonzero collateral ratio.\")\n",
"\n",
" liquidatable = list(filter(lambda mam: mam.balance_sheet.collateral_ratio <= group.maint_coll_ratio, nonzero))\n",
" self.logger.info(f\"Of those {len(nonzero)}, {len(liquidatable)} are liquidatable.\")\n",
"\n",
" above_water = list(filter(lambda mam: mam.collateral_ratio > 1, liquidatable))\n",
" self.logger.info(f\"Of those {len(liquidatable)} liquidatable margin accounts, {len(above_water)} are 'above water' margin accounts with assets greater than their liabilities.\")\n",
"\n",
" worthwhile = list(filter(lambda mam: mam.assets - mam.liabilities > self.worthwhile_threshold, above_water))\n",
" self.logger.info(f\"Of those {len(above_water)} above water margin accounts, {len(worthwhile)} are worthwhile margin accounts with more than ${self.worthwhile_threshold} net assets.\")\n",
"\n",
" highest_first = sorted(worthwhile, key=lambda mam: mam.assets - mam.liabilities, reverse=True)\n",
" if len(highest_first) == 0:\n",
2021-04-29 09:20:46 -07:00
" self.logger.info(\"No accounts to liquidate.\")\n",
" return\n",
"\n",
" for mam in highest_first:\n",
" balances_before = group.fetch_balances(self.wallet.address)\n",
" self.logger.info(\"Wallet balances before:\")\n",
" TokenValue.report(self.logger.info, balances_before)\n",
"\n",
" self.logger.info(f\"Margin account balances before:\\n{mam.balances}\")\n",
" self.logger.info(f\"Liquidating margin account: {mam.margin_account}\\n{mam.balance_sheet}\")\n",
" transaction_id = self.account_liquidator.liquidate(group, mam.margin_account, prices)\n",
" if transaction_id is None:\n",
" self.logger.info(\"No transaction sent.\")\n",
" else:\n",
" self.logger.info(f\"Transaction ID: {transaction_id} - waiting for confirmation...\")\n",
"\n",
" self.context.wait_for_confirmation(transaction_id)\n",
"\n",
" group_after = Group.load(self.context)\n",
" margin_account_after_liquidation = MarginAccount.load(self.context, mam.margin_account.address, group_after)\n",
" intrinsic_balances_after = margin_account_after_liquidation.get_intrinsic_balances(group_after)\n",
" self.logger.info(f\"Margin account balances after: {intrinsic_balances_after}\")\n",
"\n",
" self.logger.info(\"Wallet Balances After:\")\n",
" balances_after = group_after.fetch_balances(self.wallet.address)\n",
" TokenValue.report(self.logger.info, balances_after)\n",
"\n",
" self.logger.info(\"Wallet Balances Changes:\")\n",
" changes = TokenValue.changes(balances_before, balances_after)\n",
" TokenValue.report(self.logger.info, changes)\n",
"\n",
" self.wallet_balancer(prices)\n"
]
},
{
"cell_type": "markdown",
"id": "olive-revolution",
"metadata": {},
"source": [
"# 🏃 Running"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "extended-uruguay",
"metadata": {},
"outputs": [],
"source": [
"if __name__ == \"__main__\":\n",
" logging.getLogger().setLevel(logging.INFO)\n",
"\n",
" from AccountLiquidator import NullAccountLiquidator\n",
" from Context import default_context\n",
" from Wallet import default_wallet\n",
" from WalletBalancer import NullWalletBalancer\n",
"\n",
" if default_wallet is None:\n",
2021-04-29 09:20:46 -07:00
" print(\"No default wallet file available.\")\n",
" else:\n",
" liquidator = SimpleLiquidator(default_context, default_wallet, NullAccountLiquidator(), NullWalletBalancer())\n",
" liquidator.run()\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"
}
},
"nbformat": 4,
"nbformat_minor": 5
}