Added 'dry run' option to commands that perform transactions.

This commit is contained in:
Geoff Taylor 2021-05-06 15:27:25 +01:00
parent 3db09e922c
commit fab0d909c2
11 changed files with 231 additions and 105 deletions

View File

@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "markdown",
"id": "nominated-blind",
"id": "scenic-confidence",
"metadata": {},
"source": [
"# ⚠ Warning\n",
@ -16,18 +16,22 @@
},
{
"cell_type": "markdown",
"id": "coupled-butterfly",
"id": "loved-undergraduate",
"metadata": {},
"source": [
"# 🥭 AccountLiquidator\n",
"\n",
"An `AccountLiquidator` takes a `MarginAccount` and liquidates it (if possible)."
"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": "acceptable-porter",
"id": "hearing-reset",
"metadata": {
"jupyter": {
"source_hidden": true
@ -35,6 +39,7 @@
},
"outputs": [],
"source": [
"import abc\n",
"import logging\n",
"import typing\n",
"\n",
@ -48,40 +53,92 @@
},
{
"cell_type": "markdown",
"id": "scientific-origin",
"id": "united-companion",
"metadata": {},
"source": [
"# 💧 AccountLiquidator class\n",
"## 💧 AccountLiquidator class\n",
"\n",
"This base class takes a `MarginAccount` and liquidates it.\n",
"\n",
"Derived classes may override `prepare_instructions()` to extend the liquidation process (for example to cancel outstanding orders before liquidating)."
"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": "remarkable-discretion",
"id": "passive-terrorism",
"metadata": {},
"outputs": [],
"source": [
"class AccountLiquidator:\n",
"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, 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": "short-compiler",
"metadata": {},
"source": [
"## 🌬️ NullAccountLiquidator class\n",
"\n",
"A 'null', 'no-op', 'dry run' implementation of the `AccountLiquidator` class."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "wireless-tobacco",
"metadata": {},
"outputs": [],
"source": [
"class NullAccountLiquidator(AccountLiquidator):\n",
" def __init__(self, context: Context, wallet: Wallet, group: Group):\n",
" super().__init__()\n",
"\n",
" def liquidate(self, 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": "danish-recycling",
"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": "orange-cocktail",
"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",
" self.group = group\n",
"\n",
" def prepare_instructions(self, margin_account: MarginAccount, prices: typing.List[TokenValue]) -> typing.List[InstructionBuilder]:\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, self.group, self.wallet, margin_account, prices)\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, margin_account: MarginAccount, prices: typing.List[TokenValue]) -> typing.Optional[str]:\n",
" instructions = self.prepare_instructions(margin_account, prices)\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",
@ -99,13 +156,14 @@
" self.logger.debug(\" Program ID:\", instruction.program_id)\n",
"\n",
" transaction_response = self.context.client.send_transaction(transaction, self.wallet.account)\n",
" return transaction_response[\"result\"]\n",
" transaction_id = self.context.unwrap_transaction_id_or_raise_exception(transaction_response)\n",
" return transaction_id\n",
"\n"
]
},
{
"cell_type": "markdown",
"id": "otherwise-framework",
"id": "objective-lindsay",
"metadata": {},
"source": [
"# 🌪️ ForceCancelOrdersAccountLiquidator class\n",
@ -122,33 +180,33 @@
{
"cell_type": "code",
"execution_count": null,
"id": "arctic-issue",
"id": "legislative-forty",
"metadata": {},
"outputs": [],
"source": [
"class ForceCancelOrdersAccountLiquidator(AccountLiquidator):\n",
" def __init__(self, context: Context, wallet: Wallet, group: Group):\n",
" super().__init__(context, wallet, group)\n",
"class ForceCancelOrdersAccountLiquidator(ActualAccountLiquidator):\n",
" def __init__(self, context: Context, wallet: Wallet):\n",
" super().__init__(context, wallet)\n",
"\n",
" def prepare_instructions(self, margin_account: MarginAccount, prices: typing.List[TokenValue]) -> typing.List[InstructionBuilder]:\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(self.group.markets):\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, self.group, self.wallet, margin_account, market_metadata, order_count)\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(margin_account, prices)\n",
" all_instructions = force_cancel_orders_instructions + super().prepare_instructions(group, margin_account, prices)\n",
"\n",
" return all_instructions\n"
]
},
{
"cell_type": "markdown",
"id": "biblical-survey",
"id": "funny-ranking",
"metadata": {},
"source": [
"# 🏃 Running"
@ -157,7 +215,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "moved-penny",
"id": "waiting-general",
"metadata": {},
"outputs": [],
"source": [
@ -173,13 +231,12 @@
" 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",
" margin_account = margin_accounts[0]\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",
" account_liquidator = AccountLiquidator(default_context, default_wallet, group)\n",
" print(account_liquidator.prepare_instructions(margin_account, prices))\n",
"\n",
" force_cancel_orders_account_liquidator = ForceCancelOrdersAccountLiquidator(default_context, default_wallet, group)\n",
" print(force_cancel_orders_account_liquidator.prepare_instructions(margin_account, prices))\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"
]
}
],

View File

@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "markdown",
"id": "advisory-questionnaire",
"id": "urban-network",
"metadata": {},
"source": [
"# ⚠ Warning\n",
@ -16,7 +16,7 @@
},
{
"cell_type": "markdown",
"id": "expanded-season",
"id": "amber-dairy",
"metadata": {},
"source": [
"# 🥭 Instructions\n",
@ -27,7 +27,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "italian-nelson",
"id": "romance-extent",
"metadata": {
"jupyter": {
"source_hidden": true
@ -55,7 +55,7 @@
},
{
"cell_type": "markdown",
"id": "elder-canada",
"id": "dirty-valuation",
"metadata": {},
"source": [
"# InstructionBuilder class\n",
@ -66,7 +66,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "aerial-police",
"id": "amber-continuity",
"metadata": {},
"outputs": [],
"source": [
@ -85,7 +85,7 @@
},
{
"cell_type": "markdown",
"id": "trained-office",
"id": "alert-found",
"metadata": {},
"source": [
"# ForceCancelOrdersInstructionBuilder class"
@ -93,7 +93,7 @@
},
{
"cell_type": "markdown",
"id": "controlling-prince",
"id": "empirical-department",
"metadata": {},
"source": [
"## Rust Interface\n",
@ -125,7 +125,7 @@
},
{
"cell_type": "markdown",
"id": "reflected-energy",
"id": "dependent-truck",
"metadata": {},
"source": [
"## Client API call\n",
@ -169,7 +169,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "protective-frequency",
"id": "distant-johns",
"metadata": {},
"outputs": [],
"source": [
@ -275,7 +275,7 @@
},
{
"cell_type": "markdown",
"id": "surface-vessel",
"id": "passing-canberra",
"metadata": {},
"source": [
"# LiquidateInstructionBuilder class\n",
@ -287,7 +287,7 @@
},
{
"cell_type": "markdown",
"id": "confidential-house",
"id": "active-essay",
"metadata": {},
"source": [
"## Rust Interface\n",
@ -333,7 +333,7 @@
},
{
"cell_type": "markdown",
"id": "professional-memorial",
"id": "amino-duncan",
"metadata": {},
"source": [
"## Client API call\n",
@ -370,7 +370,7 @@
},
{
"cell_type": "markdown",
"id": "voluntary-communications",
"id": "according-benefit",
"metadata": {},
"source": [
"## from_margin_account_and_market() function\n",
@ -391,7 +391,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "becoming-lawsuit",
"id": "labeled-mattress",
"metadata": {},
"outputs": [],
"source": [
@ -490,7 +490,7 @@
},
{
"cell_type": "markdown",
"id": "extended-hindu",
"id": "accompanied-drill",
"metadata": {},
"source": [
"# 🏃 Running"
@ -499,7 +499,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "broadband-boost",
"id": "complicated-brother",
"metadata": {},
"outputs": [],
"source": [

View File

@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "markdown",
"id": "consistent-ceramic",
"id": "satellite-eleven",
"metadata": {},
"source": [
"# ⚠ Warning\n",
@ -16,7 +16,7 @@
},
{
"cell_type": "markdown",
"id": "sound-transparency",
"id": "taken-sound",
"metadata": {},
"source": [
"# 🥭 Liquidation\n",
@ -31,7 +31,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "ordered-share",
"id": "shaped-plasma",
"metadata": {
"jupyter": {
"source_hidden": true
@ -49,7 +49,7 @@
},
{
"cell_type": "markdown",
"id": "respective-indonesian",
"id": "cosmetic-province",
"metadata": {},
"source": [
"## 🦺 Safety\n",
@ -63,7 +63,7 @@
},
{
"cell_type": "markdown",
"id": "valued-bernard",
"id": "analyzed-pollution",
"metadata": {},
"source": [
"## 📇 Collateralisation Ratios Details\n",
@ -81,7 +81,7 @@
},
{
"cell_type": "markdown",
"id": "direct-fundamental",
"id": "double-cross",
"metadata": {},
"source": [
"# 💧 Liquidation Process"
@ -89,7 +89,7 @@
},
{
"cell_type": "markdown",
"id": "received-distance",
"id": "indie-villa",
"metadata": {},
"source": [
"## 📇 Steps\n",
@ -117,7 +117,7 @@
},
{
"cell_type": "markdown",
"id": "gothic-qualification",
"id": "downtown-horse",
"metadata": {},
"source": [
"# 🏃 Running\n",
@ -134,7 +134,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "celtic-python",
"id": "patent-lodging",
"metadata": {},
"outputs": [],
"source": [
@ -144,7 +144,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "expected-fellow",
"id": "solved-summit",
"metadata": {},
"outputs": [],
"source": [
@ -170,8 +170,8 @@
" margin_account = MarginAccount.load(default_context, PublicKey(MARGIN_ACCOUNT_TO_LIQUIDATE), group)\n",
" intrinsic_balance_sheets_before = margin_account.get_intrinsic_balance_sheets(group)\n",
" print(\"Margin Account Before:\", intrinsic_balance_sheets_before)\n",
" liquidator = ForceCancelOrdersAccountLiquidator(default_context, default_wallet, group)\n",
" transaction_id = liquidator.liquidate(margin_account, prices)\n",
" liquidator = ForceCancelOrdersAccountLiquidator(default_context, default_wallet)\n",
" transaction_id = liquidator.liquidate(group, margin_account, prices)\n",
" if transaction_id is None:\n",
" print(\"No transaction sent.\")\n",
" else:\n",

View File

@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "markdown",
"id": "rotary-tsunami",
"id": "executed-lithuania",
"metadata": {},
"source": [
"# ⚠ Warning\n",
@ -16,7 +16,7 @@
},
{
"cell_type": "markdown",
"id": "imperial-observation",
"id": "latest-vacuum",
"metadata": {},
"source": [
"# 🥭 SimpleLiquidator\n",
@ -27,7 +27,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "clear-ultimate",
"id": "rising-graham",
"metadata": {
"jupyter": {
"source_hidden": true
@ -38,7 +38,7 @@
"import logging\n",
"import typing\n",
"\n",
"from AccountLiquidator import ForceCancelOrdersAccountLiquidator\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",
@ -47,7 +47,7 @@
},
{
"cell_type": "markdown",
"id": "nuclear-greenhouse",
"id": "disciplinary-plant",
"metadata": {},
"source": [
"# SimpleLiquidator class\n",
@ -69,27 +69,28 @@
{
"cell_type": "code",
"execution_count": null,
"id": "verified-boring",
"id": "adjacent-burton",
"metadata": {},
"outputs": [],
"source": [
"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",
"class SimpleLiquidator:\n",
" def __init__(self, context: Context, wallet: Wallet, wallet_balancer: WalletBalancer, worthwhile_threshold: float = 0.01):\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",
@ -122,13 +123,12 @@
" self.logger.info(\"No accounts to liquidate.\")\n",
" return\n",
"\n",
" liquidator = ForceCancelOrdersAccountLiquidator(self.context, self.wallet, group)\n",
" for mam in highest_first:\n",
" wallet_balances_before = token_balances_from_wallet(self.context, self.wallet, group)\n",
" wallet_balances_before = _token_balances_from_wallet(self.context, self.wallet, group)\n",
" self.logger.info(f\"Wallet balances before:\\n{wallet_balances_before}\")\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 = liquidator.liquidate(mam.margin_account, prices)\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",
@ -140,14 +140,14 @@
" 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",
" wallet_balances_after = token_balances_from_wallet(self.context, self.wallet, group)\n",
" wallet_balances_after = _token_balances_from_wallet(self.context, self.wallet, group)\n",
" self.logger.info(f\"Wallet balances after:\\n{wallet_balances_after}\")\n",
" self.wallet_balancer(prices)\n"
]
},
{
"cell_type": "markdown",
"id": "colored-stable",
"id": "nutritional-virginia",
"metadata": {},
"source": [
"# 🏃 Running"
@ -156,13 +156,14 @@
{
"cell_type": "code",
"execution_count": null,
"id": "frank-decade",
"id": "unable-means",
"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",
@ -170,7 +171,7 @@
" if default_wallet is None:\n",
" print(\"No default wallet file available.\")\n",
" else:\n",
" liquidator = SimpleLiquidator(default_context, default_wallet, NullWalletBalancer())\n",
" liquidator = SimpleLiquidator(default_context, default_wallet, NullAccountLiquidator(), NullWalletBalancer())\n",
" liquidator.run()\n"
]
}

View File

@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "markdown",
"id": "minor-wisconsin",
"id": "angry-beast",
"metadata": {},
"source": [
"# ⚠ Warning\n",
@ -16,7 +16,7 @@
},
{
"cell_type": "markdown",
"id": "electronic-michael",
"id": "corporate-basis",
"metadata": {},
"source": [
"# 🥭 TradeExecutor\n",
@ -31,7 +31,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "rubber-latin",
"id": "driven-masters",
"metadata": {
"jupyter": {
"source_hidden": true
@ -59,7 +59,7 @@
},
{
"cell_type": "markdown",
"id": "historic-auckland",
"id": "complicated-ebony",
"metadata": {},
"source": [
"# TradeExecutor class\n",
@ -77,7 +77,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "combined-spank",
"id": "tribal-strategy",
"metadata": {},
"outputs": [],
"source": [
@ -96,7 +96,35 @@
},
{
"cell_type": "markdown",
"id": "surgical-tradition",
"id": "convertible-domain",
"metadata": {},
"source": [
"## NullTradeExecutor class\n",
"\n",
"A null, no-op, dry-run trade executor that can be plugged in anywhere a `TradeExecutor` is expected, but which will not actually trade."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "bored-bulgarian",
"metadata": {},
"outputs": [],
"source": [
"class NullTradeExecutor(TradeExecutor):\n",
" def __init__(self):\n",
" super().__init__()\n",
"\n",
" def buy(self, symbol: str, quantity: Decimal):\n",
" self.logger.info(f\"Skipping BUY trade of {quantity:,.8f} of '{symbol}'.\")\n",
"\n",
" def sell(self, symbol: str, quantity: Decimal):\n",
" self.logger.info(f\"Skipping SELL trade of {quantity:,.8f} of '{symbol}'.\")\n"
]
},
{
"cell_type": "markdown",
"id": "alien-extent",
"metadata": {},
"source": [
"# SerumImmediateTradeExecutor class\n",
@ -126,7 +154,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "finished-craps",
"id": "ceramic-newport",
"metadata": {},
"outputs": [],
"source": [
@ -295,7 +323,7 @@
},
{
"cell_type": "markdown",
"id": "polar-chance",
"id": "developmental-internet",
"metadata": {},
"source": [
"# 🏃 Running\n",
@ -308,7 +336,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "welsh-pension",
"id": "consecutive-pasta",
"metadata": {},
"outputs": [],
"source": [

View File

@ -30,7 +30,7 @@ from decimal import Decimal
from BaseModel import Group
from Constants import WARNING_DISCLAIMER_TEXT
from Context import Context, default_cluster, default_cluster_url, default_program_id, default_dex_program_id, default_group_name, default_group_id
from TradeExecutor import SerumImmediateTradeExecutor
from TradeExecutor import NullTradeExecutor, SerumImmediateTradeExecutor, TradeExecutor
from Wallet import Wallet
from WalletBalancer import TargetBalanceParser, WalletBalancer
@ -59,6 +59,8 @@ 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("--dry-run", type=bool,
help="runs as read-only and does not perform any transactions")
args = parser.parse_args()
logging.getLogger().setLevel(args.log_level)
@ -88,7 +90,10 @@ try:
prices = group.fetch_token_prices()
print(prices)
trade_executor = SerumImmediateTradeExecutor(context, wallet, group, adjustment_factor)
if args.dry_run:
trade_executor: TradeExecutor = NullTradeExecutor()
else:
trade_executor = SerumImmediateTradeExecutor(context, wallet, group, adjustment_factor)
wallet_balancer = WalletBalancer(context, wallet, trade_executor, action_threshold, group.tokens, targets)
wallet_balancer.balance(prices)

View File

@ -30,7 +30,7 @@ from decimal import Decimal
from BaseModel import Group
from Constants import WARNING_DISCLAIMER_TEXT
from Context import Context, default_cluster, default_cluster_url, default_program_id, default_dex_program_id, default_group_name, default_group_id
from TradeExecutor import SerumImmediateTradeExecutor
from TradeExecutor import NullTradeExecutor, SerumImmediateTradeExecutor, TradeExecutor
from Wallet import Wallet
# We explicitly want argument parsing to be outside the main try-except block because some arguments
@ -56,6 +56,8 @@ parser.add_argument("--token-symbol", type=str, required=True, help="token symbo
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("--dry-run", type=bool,
help="runs as read-only and does not perform any transactions")
args = parser.parse_args()
logging.getLogger().setLevel(args.log_level)
@ -79,7 +81,11 @@ try:
symbol = args.token_symbol.upper()
trade_executor = SerumImmediateTradeExecutor(context, wallet, group, adjustment_factor)
if args.dry_run:
trade_executor: TradeExecutor = NullTradeExecutor()
else:
trade_executor = SerumImmediateTradeExecutor(context, wallet, group, adjustment_factor)
trade_executor.buy(symbol, args.quantity)
logging.info("Buy completed.")

View File

@ -30,7 +30,7 @@ from decimal import Decimal
from BaseModel import Group
from Constants import WARNING_DISCLAIMER_TEXT
from Context import Context, default_cluster, default_cluster_url, default_program_id, default_dex_program_id, default_group_name, default_group_id
from TradeExecutor import SerumImmediateTradeExecutor
from TradeExecutor import NullTradeExecutor, SerumImmediateTradeExecutor, TradeExecutor
from Wallet import Wallet
# We explicitly want argument parsing to be outside the main try-except block because some arguments
@ -56,6 +56,8 @@ parser.add_argument("--token-symbol", type=str, required=True, help="token symbo
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("--dry-run", type=bool,
help="runs as read-only and does not perform any transactions")
args = parser.parse_args()
logging.getLogger().setLevel(args.log_level)
@ -79,7 +81,11 @@ try:
symbol = args.token_symbol.upper()
trade_executor = SerumImmediateTradeExecutor(context, wallet, group, adjustment_factor)
if args.dry_run:
trade_executor: TradeExecutor = NullTradeExecutor()
else:
trade_executor = SerumImmediateTradeExecutor(context, wallet, group, adjustment_factor)
trade_executor.sell(symbol, args.quantity)
logging.info("Sell completed.")

View File

@ -28,7 +28,7 @@ import traceback
from solana.publickey import PublicKey
from AccountScout import AccountScout
from AccountLiquidator import ForceCancelOrdersAccountLiquidator
from AccountLiquidator import AccountLiquidator, ForceCancelOrdersAccountLiquidator, NullAccountLiquidator
from BaseModel import Group, MarginAccount
from Constants import WARNING_DISCLAIMER_TEXT
from Context import Context, default_cluster, default_cluster_url, default_program_id, default_dex_program_id, default_group_name, default_group_id
@ -55,6 +55,8 @@ parser.add_argument("--log-level", default=logging.INFO, type=lambda level: geta
help="level of verbosity to log (possible values: DEBUG, INFO, WARNING, ERROR, CRITICAL)")
parser.add_argument('--margin-account-address', type=PublicKey,
help="Solana address of the Mango Markets margin account to be liquidated")
parser.add_argument("--dry-run", type=bool,
help="runs as read-only and does not perform any transactions")
args = parser.parse_args()
logging.getLogger().setLevel(args.log_level)
@ -96,8 +98,13 @@ try:
margin_account = MarginAccount.load(context, margin_account_address, group)
intrinsic_balance_sheets_before = margin_account.get_intrinsic_balance_sheets(group)
print("Margin Account Before:", intrinsic_balance_sheets_before)
liquidator = ForceCancelOrdersAccountLiquidator(context, wallet, group)
transaction_id = liquidator.liquidate(margin_account, prices)
if args.dry_run:
account_liquidator: AccountLiquidator = NullAccountLiquidator()
else:
account_liquidator = ForceCancelOrdersAccountLiquidator(context, wallet)
transaction_id = account_liquidator.liquidate(group, margin_account, prices)
if transaction_id is None:
print("No transaction sent.")
else:

View File

@ -29,6 +29,7 @@ import traceback
from decimal import Decimal
from AccountScout import AccountScout
from AccountLiquidator import AccountLiquidator, ForceCancelOrdersAccountLiquidator, NullAccountLiquidator
from BaseModel import Group
from Constants import WARNING_DISCLAIMER_TEXT
from Context import Context, default_cluster, default_cluster_url, default_program_id, default_dex_program_id, default_group_name, default_group_id
@ -64,6 +65,8 @@ 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("--dry-run", type=bool,
help="runs as read-only and does not perform any transactions")
args = parser.parse_args()
logging.getLogger().setLevel(args.log_level)
@ -97,7 +100,12 @@ try:
logging.info("Wallet accounts OK.")
if (args.target is None) or (len(args.target) == 0):
if args.dry_run:
account_liquidator: AccountLiquidator = NullAccountLiquidator()
else:
account_liquidator = ForceCancelOrdersAccountLiquidator(context, wallet)
if args.dry_run or (args.target is None) or (len(args.target) == 0):
wallet_balancer = NullWalletBalancer()
else:
balance_parser = TargetBalanceParser(group.tokens)
@ -106,7 +114,7 @@ try:
wallet_balancer = LiveWalletBalancer(context, wallet, trade_executor, action_threshold, group.tokens, targets)
stop = False
liquidator = SimpleLiquidator(context, wallet, wallet_balancer)
liquidator = SimpleLiquidator(context, wallet, account_liquidator, wallet_balancer)
while not stop:
started_at = time.time()
try:

View File

@ -26,6 +26,7 @@ import projectsetup
import traceback
from AccountScout import AccountScout
from AccountLiquidator import AccountLiquidator, ForceCancelOrdersAccountLiquidator, NullAccountLiquidator
from BaseModel import Group
from Constants import WARNING_DISCLAIMER_TEXT
from Context import Context, default_cluster, default_cluster_url, default_program_id, default_dex_program_id, default_group_name, default_group_id
@ -52,6 +53,8 @@ parser.add_argument("--id-file", type=str, default="id.json",
help="file containing the JSON-formatted wallet private key")
parser.add_argument("--log-level", default=logging.INFO, type=lambda level: getattr(logging, level),
help="level of verbosity to log (possible values: DEBUG, INFO, WARNING, ERROR, CRITICAL)")
parser.add_argument("--dry-run", type=bool,
help="runs as read-only and does not perform any transactions")
args = parser.parse_args()
logging.getLogger().setLevel(args.log_level)
@ -81,8 +84,13 @@ try:
logging.info("Wallet accounts OK.")
if args.dry_run:
account_liquidator: AccountLiquidator = NullAccountLiquidator()
else:
account_liquidator = ForceCancelOrdersAccountLiquidator(context, wallet)
wallet_balancer = NullWalletBalancer()
liquidator = SimpleLiquidator(context, wallet, wallet_balancer)
liquidator = SimpleLiquidator(context, wallet, account_liquidator, wallet_balancer)
liquidator.run()
except Exception as exception: