diff --git a/bin/liquidate-single-account b/bin/liquidate-single-account index 162dd96..72e6c35 100755 --- a/bin/liquidate-single-account +++ b/bin/liquidate-single-account @@ -46,12 +46,12 @@ logging.warning(mango.WARNING_DISCLAIMER_TEXT) try: context = mango.Context.from_command_line_parameters(args) wallet = mango.Wallet.from_command_line_parameters_or_raise(args) - margin_account_address = args.address + account_address = args.address liquidator_name = args.name logging.info(f"Context: {context}") logging.info(f"Wallet address: {wallet.address}") - logging.info(f"Margin account address: {margin_account_address}") + logging.info(f"Margin account address: {account_address}") group = mango.Group.load(context) @@ -86,9 +86,9 @@ try: # TODO - fetch prices when available for V3. # prices = group.fetch_token_prices(context) - margin_account = mango.Account.load(context, margin_account_address, group) + 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, [], margin_account, worthwhile_threshold) + liquidatable_report = mango.LiquidatableReport.build(group, [], account, worthwhile_threshold) transaction_id = account_liquidator.liquidate(liquidatable_report) if transaction_id is None: print("No transaction sent.") diff --git a/bin/liquidator b/bin/liquidator index a82a8fb..30af32d 100755 --- a/bin/liquidator +++ b/bin/liquidator @@ -58,17 +58,17 @@ for notify in args.notify_errors: logging.warning(mango.WARNING_DISCLAIMER_TEXT) -def start_subscriptions(context: mango.Context, liquidation_processor: mango.LiquidationProcessor, fetch_prices: typing.Callable[[typing.Any], typing.Any], fetch_margin_accounts: typing.Callable[[typing.Any], typing.Any], throttle_reload_to_seconds: Decimal, throttle_ripe_update_to_seconds: Decimal): +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): liquidation_processor.state = mango.LiquidationProcessorState.STARTING logging.info("Starting margin account fetcher subscription") - margin_account_subscription = rx.interval(float(throttle_reload_to_seconds)).pipe( + account_subscription = rx.interval(float(throttle_reload_to_seconds)).pipe( ops.subscribe_on(context.pool_scheduler), ops.start_with(-1), - ops.map(fetch_margin_accounts(context)), + 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_margin_accounts, on_error=mango.log_subscription_error)) + ).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( @@ -78,7 +78,7 @@ def start_subscriptions(context: mango.Context, liquidation_processor: mango.Liq 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 margin_account_subscription, price_subscription + return account_subscription, price_subscription try: @@ -160,29 +160,29 @@ try: return _fetch_prices - def fetch_margin_accounts(context): + def fetch_accounts(context): def _actual_fetch(): group = mango.Group.load(context) return mango.Account.load_ripe(context, group) - def _fetch_margin_accounts(_): + def _fetch_accounts(_): with mango.retry_context("Margin Account Fetch", _actual_fetch, context.retry_pauses) as retrier: return retrier.run() - return _fetch_margin_accounts + return _fetch_accounts class LiquidationProcessorSubscriptions: - def __init__(self, margin_account: rx.core.typing.Disposable, price: rx.core.typing.Disposable): - self.margin_account: rx.core.typing.Disposable = margin_account + def __init__(self, account: rx.core.typing.Disposable, price: rx.core.typing.Disposable): + 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) - margin_account_subscription, price_subscription = start_subscriptions( - context, liquidation_processor, fetch_prices, fetch_margin_accounts, throttle_reload_to_seconds, throttle_ripe_update_to_seconds) + account_subscription, price_subscription = start_subscriptions( + context, liquidation_processor, fetch_prices, fetch_accounts, throttle_reload_to_seconds, throttle_ripe_update_to_seconds) - subscriptions = LiquidationProcessorSubscriptions(margin_account=margin_account_subscription, + subscriptions = LiquidationProcessorSubscriptions(account=account_subscription, price=price_subscription) def on_unhealthy(liquidation_processor: mango.LiquidationProcessor): @@ -193,7 +193,7 @@ try: logging.warning("Liquidation processor has been marked as unhealthy so recreating subscriptions.") try: - subscriptions.margin_account.dispose() + subscriptions.account.dispose() except Exception as exception: logging.warning(f"Ignoring problem disposing of margin account subscription: {exception}") try: @@ -201,9 +201,9 @@ try: except Exception as exception: logging.warning(f"Ignoring problem disposing of margin account subscription: {exception}") - margin_account_subscription, price_subscription = start_subscriptions( - context, liquidation_processor, fetch_prices, fetch_margin_accounts, throttle_reload_to_seconds, throttle_ripe_update_to_seconds) - subscriptions.margin_account = margin_account_subscription + account_subscription, price_subscription = start_subscriptions( + context, liquidation_processor, fetch_prices, fetch_accounts, throttle_reload_to_seconds, throttle_ripe_update_to_seconds) + subscriptions.account = account_subscription subscriptions.price = price_subscription liquidation_processor.state_change.subscribe(on_next=on_unhealthy) diff --git a/bin/liquidator-single-run b/bin/liquidator-single-run index c050774..b78bac5 100755 --- a/bin/liquidator-single-run +++ b/bin/liquidator-single-run @@ -84,8 +84,7 @@ try: liquidation_processor = mango.LiquidationProcessor(context, liquidator_name, account_liquidator, wallet_balancer) started_at = time.time() - # ripe = group.load_ripe_margin_accounts() - liquidation_processor.update_margin_accounts([]) + liquidation_processor.update_accounts([]) group = mango.Group.load(context) # Refresh group data # prices = group.fetch_token_prices(context) diff --git a/bin/withdraw b/bin/withdraw index ee8ea9e..8d9524b 100755 --- a/bin/withdraw +++ b/bin/withdraw @@ -30,7 +30,7 @@ group = mango.Group.load(context, context.group_id) accounts = mango.Account.load_all_for_owner(context, wallet.address, group) if len(accounts) == 0: raise Exception(f"Could not find any margin accounts for '{wallet.address}'.") -margin_account = accounts[0] +account = accounts[0] token = context.token_lookup.find_by_symbol(args.symbol) if token is None: @@ -53,7 +53,7 @@ node_bank = root_bank.pick_node_bank(context) signers: mango.CombinableInstructions = mango.CombinableInstructions.from_wallet(wallet) withdraw = mango.build_withdraw_instructions( - context, wallet, group, margin_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_id = all_instructions.execute_and_unwrap_transaction_ids(context)[0] diff --git a/mango/accountscout.py b/mango/accountscout.py index 49487af..2d247ca 100644 --- a/mango/accountscout.py +++ b/mango/accountscout.py @@ -155,12 +155,12 @@ class AccountScout: f"Account '{account_address}' has {len(token_accounts)} {basket_token.token.name} token account(s) with mint '{basket_token.token.mint}': {[ta.address for ta in token_accounts]}") # May have one or more Mango Markets margin account, but it's optional for liquidating - margin_accounts = Account.load_all_for_owner(context, account_address, group) - if len(margin_accounts) == 0: + 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.") else: - for margin_account in margin_accounts: - report.add_detail(f"Margin account: {margin_account}") + for account in accounts: + report.add_detail(f"Margin account: {account}") return report diff --git a/mango/createmarketoperations.py b/mango/createmarketoperations.py index 794fdec..b3105ce 100644 --- a/mango/createmarketoperations.py +++ b/mango/createmarketoperations.py @@ -22,11 +22,14 @@ from .group import Group from .market import Market from .marketoperations import MarketOperations, NullMarketOperations from .perpmarket import PerpMarket +from .perpmarketinstructionbuilder import PerpMarketInstructionBuilder from .perpmarketoperations import PerpMarketOperations from .perpsmarket import PerpsMarket from .serummarket import SerumMarket +from .serummarketinstructionbuilder import SerumMarketInstructionBuilder from .serummarketoperations import SerumMarketOperations from .spotmarket import SpotMarket +from .spotmarketinstructionbuilder import SpotMarketInstructionBuilder from .spotmarketoperations import SpotMarketOperations from .wallet import Wallet @@ -39,18 +42,27 @@ def create_market_operations(context: Context, wallet: Wallet, dry_run: bool, ma if dry_run: return NullMarketOperations(market.symbol, reporter) elif isinstance(market, SerumMarket): - return SerumMarketOperations(context, wallet, market, reporter) + serum_market_instruction_builder: SerumMarketInstructionBuilder = SerumMarketInstructionBuilder.load( + context, wallet, market) + return SerumMarketOperations(context, wallet, market, serum_market_instruction_builder, reporter) elif isinstance(market, SpotMarket): group = Group.load(context, market.group_address) - margin_accounts = Account.load_all_for_owner(context, wallet.address, group) - return SpotMarketOperations(context, wallet, group, margin_accounts[0], market, reporter) + accounts = Account.load_all_for_owner(context, wallet.address, group) + account = accounts[0] + spot_market_instruction_builder: SpotMarketInstructionBuilder = SpotMarketInstructionBuilder.load( + context, wallet, group, account, market) + return SpotMarketOperations(context, wallet, group, account, market, spot_market_instruction_builder, reporter) elif isinstance(market, PerpsMarket): group = Group.load(context, context.group_id) - margin_accounts = Account.load_all_for_owner(context, wallet.address, group) + accounts = Account.load_all_for_owner(context, wallet.address, group) + account = accounts[0] perp_market_info = group.perp_markets[0] if perp_market_info is None: raise Exception("Perp market not found at index 0.") perp_market = PerpMarket.load(context, perp_market_info.address, group) - return PerpMarketOperations(market.symbol, context, wallet, margin_accounts[0], perp_market, reporter) + perp_market_instruction_builder: PerpMarketInstructionBuilder = PerpMarketInstructionBuilder.load( + context, wallet, group, account, perp_market) + + return PerpMarketOperations(market.symbol, context, wallet, perp_market_instruction_builder, account, perp_market, reporter) else: raise Exception(f"Could not find order placer for market {market.symbol}") diff --git a/mango/group.py b/mango/group.py index 25c74e4..7b1634a 100644 --- a/mango/group.py +++ b/mango/group.py @@ -119,6 +119,20 @@ class Group(AddressableAccount): raise Exception(f"Group account not found at address '{group_address}'") return Group.parse(context, account_info) + def find_spot_market_index(self, spot_market_address: PublicKey) -> int: + for index, spot in enumerate(self.spot_markets): + if spot is not None and spot.address == spot_market_address: + return index + + raise Exception(f"Could not find spot market {spot_market_address} in group {self.address}") + + def find_perp_market_index(self, perp_market_address: PublicKey) -> int: + for index, pm in enumerate(self.perp_markets): + if pm is not None and pm.address == perp_market_address: + return index + + raise Exception(f"Could not find perp market {perp_market_address} in group {self.address}") + def fetch_balances(self, context: Context, root_address: PublicKey) -> typing.Sequence[TokenValue]: balances: typing.List[TokenValue] = [] sol_balance = context.fetch_sol_balance(root_address) diff --git a/mango/instructions.py b/mango/instructions.py index bf613a5..40c6b6a 100644 --- a/mango/instructions.py +++ b/mango/instructions.py @@ -71,12 +71,7 @@ from .wallet import Wallet def _ensure_openorders(context: Context, wallet: Wallet, group: Group, account: Account, market: Market) -> typing.Tuple[PublicKey, CombinableInstructions]: spot_market_address = market.state.public_key() - market_index: int = -1 - for index, spot in enumerate(group.spot_markets): - if spot is not None and spot.address == spot_market_address: - market_index = index - if market_index == -1: - raise Exception(f"Could not find spot market {spot_market_address} in group {group.address}") + market_index = group.find_spot_market_index(spot_market_address) open_orders_address = account.spot_open_orders[market_index] if open_orders_address is not None: @@ -274,7 +269,7 @@ def build_compound_serum_place_order_instructions(context: Context, wallet: Wall # -def build_cancel_perp_order_instructions(context: Context, wallet: Wallet, margin_account: Account, perp_market: PerpMarket, order: Order) -> CombinableInstructions: +def build_cancel_perp_order_instructions(context: Context, wallet: Wallet, account: Account, perp_market: PerpMarket, order: Order) -> CombinableInstructions: # Prefer cancelling by client ID so we don't have to keep track of the order side. if order.client_id != 0: data: bytes = layouts.CANCEL_PERP_ORDER_BY_CLIENT_ID.build( @@ -302,8 +297,8 @@ def build_cancel_perp_order_instructions(context: Context, wallet: Wallet, margi instructions = [ TransactionInstruction( keys=[ - AccountMeta(is_signer=False, is_writable=False, pubkey=margin_account.group.address), - AccountMeta(is_signer=False, is_writable=True, pubkey=margin_account.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.address), AccountMeta(is_signer=False, is_writable=True, pubkey=perp_market.bids), @@ -317,7 +312,7 @@ def build_cancel_perp_order_instructions(context: Context, wallet: Wallet, margi return CombinableInstructions(signers=[], instructions=instructions) -def build_place_perp_order_instructions(context: Context, wallet: Wallet, group: Group, margin_account: Account, perp_market: PerpMarket, price: Decimal, quantity: Decimal, client_order_id: int, side: Side, order_type: OrderType) -> CombinableInstructions: +def build_place_perp_order_instructions(context: Context, wallet: Wallet, group: Group, account: Account, perp_market: PerpMarket, price: Decimal, quantity: Decimal, client_order_id: int, side: Side, order_type: OrderType) -> CombinableInstructions: # { buy: 0, sell: 1 } raw_side: int = 1 if side == Side.SELL else 0 # { limit: 0, ioc: 1, postOnly: 2 } @@ -346,7 +341,7 @@ def build_place_perp_order_instructions(context: Context, wallet: Wallet, group: TransactionInstruction( keys=[ AccountMeta(is_signer=False, is_writable=False, pubkey=group.address), - AccountMeta(is_signer=False, is_writable=True, pubkey=margin_account.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=group.cache), AccountMeta(is_signer=False, is_writable=True, pubkey=perp_market.address), @@ -354,7 +349,7 @@ def build_place_perp_order_instructions(context: Context, wallet: Wallet, group: AccountMeta(is_signer=False, is_writable=True, pubkey=perp_market.asks), AccountMeta(is_signer=False, is_writable=True, pubkey=perp_market.event_queue), *list([AccountMeta(is_signer=False, is_writable=False, - pubkey=oo_address or SYSTEM_PROGRAM_ADDRESS) for oo_address in margin_account.spot_open_orders]) + pubkey=oo_address or SYSTEM_PROGRAM_ADDRESS) for oo_address in account.spot_open_orders]) ], program_id=context.program_id, data=layouts.PLACE_PERP_ORDER.build( @@ -437,12 +432,12 @@ def build_create_account_instructions(context: Context, wallet: Wallet, group: G # quantity: u64, # allow_borrow: bool, # }, -def build_withdraw_instructions(context: Context, wallet: Wallet, group: Group, margin_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=[ AccountMeta(is_signer=False, is_writable=False, pubkey=group.address), - AccountMeta(is_signer=False, is_writable=True, pubkey=margin_account.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=group.cache), AccountMeta(is_signer=False, is_writable=False, pubkey=root_bank.address), @@ -452,7 +447,7 @@ def build_withdraw_instructions(context: Context, wallet: Wallet, group: Group, 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 margin_account.spot_open_orders]) + pubkey=oo_address or SYSTEM_PROGRAM_ADDRESS) for oo_address in account.spot_open_orders]) ], program_id=context.program_id, data=layouts.WITHDRAW.build({ diff --git a/mango/liquidationevent.py b/mango/liquidationevent.py index 85c0e1e..50a3453 100644 --- a/mango/liquidationevent.py +++ b/mango/liquidationevent.py @@ -25,14 +25,14 @@ from .tokenvalue import TokenValue class LiquidationEvent: - def __init__(self, timestamp: datetime.datetime, liquidator_name: str, group_name: str, succeeded: bool, signature: str, wallet_address: PublicKey, margin_account_address: PublicKey, balances_before: typing.Sequence[TokenValue], balances_after: typing.Sequence[TokenValue]): + def __init__(self, timestamp: datetime.datetime, liquidator_name: str, group_name: str, succeeded: bool, signature: str, wallet_address: PublicKey, account_address: PublicKey, balances_before: typing.Sequence[TokenValue], balances_after: typing.Sequence[TokenValue]): self.timestamp: datetime.datetime = timestamp self.liquidator_name: str = liquidator_name self.group_name: str = group_name self.succeeded: bool = succeeded self.signature: str = signature self.wallet_address: PublicKey = wallet_address - self.margin_account_address: PublicKey = margin_account_address + self.account_address: PublicKey = account_address self.balances_before: typing.Sequence[TokenValue] = balances_before self.balances_after: typing.Sequence[TokenValue] = balances_after self.changes: typing.Sequence[TokenValue] = TokenValue.changes(balances_before, balances_after) @@ -45,7 +45,7 @@ class LiquidationEvent: ๐Ÿ—ƒ๏ธ Group: {self.group_name} ๐Ÿ“‡ Signature: {self.signature} ๐Ÿ‘› Wallet: {self.wallet_address} - ๐Ÿ’ณ Margin Account: {self.margin_account_address} + ๐Ÿ’ณ Margin Account: {self.account_address} ๐Ÿ’ธ Changes: {changes_text} ยป""" diff --git a/mango/liquidationprocessor.py b/mango/liquidationprocessor.py index ead60e5..8eb6e96 100644 --- a/mango/liquidationprocessor.py +++ b/mango/liquidationprocessor.py @@ -78,11 +78,11 @@ class LiquidationProcessor: self.state: LiquidationProcessorState = LiquidationProcessorState.STARTING self.state_change: EventSource[LiquidationProcessor] = EventSource[LiquidationProcessor]() - def update_margin_accounts(self, ripe_margin_accounts: typing.Sequence[Account]): + def update_accounts(self, ripe_accounts: typing.Sequence[Account]): self.logger.info( - f"Received {len(ripe_margin_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_margin_accounts + self.ripe_accounts = ripe_accounts self.ripe_accounts_updated_at = datetime.now() # If this is the first time through, mark ourselves as Healthy. if self.state == LiquidationProcessorState.STARTING: @@ -105,8 +105,8 @@ class LiquidationProcessor: report: typing.List[str] = [] updated: typing.List[LiquidatableReport] = [] - for margin_account in self.ripe_accounts: - updated += [LiquidatableReport.build(group, prices, margin_account, self.worthwhile_threshold)] + for account in self.ripe_accounts: + 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."] @@ -139,15 +139,15 @@ class LiquidationProcessor: self.account_liquidator.liquidate(highest) self.wallet_balancer.balance(prices) - updated_margin_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_margin_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_margin_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_margin_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( diff --git a/mango/notification.py b/mango/notification.py index dd78f9d..4c5a9d9 100644 --- a/mango/notification.py +++ b/mango/notification.py @@ -242,7 +242,7 @@ class CsvFileNotificationTarget(NotificationTarget): 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, - event.signature, event.wallet_address, event.margin_account_address] + event.signature, 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) diff --git a/mango/perpmarket.py b/mango/perpmarket.py index eaf0658..f127452 100644 --- a/mango/perpmarket.py +++ b/mango/perpmarket.py @@ -59,17 +59,11 @@ class PerpMarket(AddressableAccount): self.scaler: PublicKey = scaler self.total_liquidity_points: Decimal = total_liquidity_points - market_index = -1 - for index, pm in enumerate(group.perp_markets): - if pm is not None and pm.address == self.address: - market_index = index - if market_index == -1: - raise Exception(f"Could not find perp market {self.address} in group {group.address}") - self.market_index = market_index + self.market_index = group.find_perp_market_index(self.address) - base_token = group.tokens[market_index] + base_token = group.tokens[self.market_index] if base_token is None: - raise Exception(f"Could not find base token at index {market_index} for perp market {self.address}.") + raise Exception(f"Could not find base token at index {self.market_index} for perp market {self.address}.") self.base_token: TokenInfo = base_token quote_token = group.tokens[-1] diff --git a/mango/perpmarketoperations.py b/mango/perpmarketoperations.py index 941776e..140d8a1 100644 --- a/mango/perpmarketoperations.py +++ b/mango/perpmarketoperations.py @@ -24,10 +24,10 @@ from .accountinfo import AccountInfo from .combinableinstructions import CombinableInstructions from .context import Context from .marketoperations import MarketOperations -from .instructions import build_cancel_perp_order_instructions, build_place_perp_order_instructions from .orderbookside import OrderBookSide from .orders import Order, OrderType, Side from .perpmarket import PerpMarket +from .perpmarketinstructionbuilder import PerpMarketInstructionBuilder from .wallet import Wallet @@ -39,41 +39,35 @@ from .wallet import Wallet class PerpMarketOperations(MarketOperations): def __init__(self, market_name: str, context: Context, wallet: Wallet, - margin_account: Account, perp_market: PerpMarket, + market_instruction_builder: PerpMarketInstructionBuilder, + account: Account, perp_market: PerpMarket, reporter: typing.Callable[[str], None] = None): super().__init__() self.market_name: str = market_name self.context: Context = context self.wallet: Wallet = wallet - self.margin_account: Account = margin_account + self.market_instruction_builder: PerpMarketInstructionBuilder = market_instruction_builder + self.account: Account = account self.perp_market: PerpMarket = perp_market self.reporter = reporter or (lambda _: None) def cancel_order(self, order: Order) -> typing.Sequence[str]: - report = f"Cancelling order on market {self.market_name}." - self.logger.info(report) - self.reporter(report) - + self.reporter(f"Cancelling order {order.id} on market {self.market_name}.") signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) - cancel_instructions = build_cancel_perp_order_instructions( - self.context, self.wallet, self.margin_account, self.perp_market, order) - all_instructions = signers + cancel_instructions - - return all_instructions.execute_and_unwrap_transaction_ids(self.context) + cancel = self.market_instruction_builder.build_cancel_order_instructions(order) + return (signers + cancel).execute_and_unwrap_transaction_ids(self.context) def place_order(self, side: Side, order_type: OrderType, price: Decimal, size: Decimal) -> Order: - client_order_id = self.context.random_client_id() - report = f"Placing {order_type} {side} order for size {size} at price {price} on market {self.market_name} using client ID {client_order_id}." + client_id: int = self.context.random_client_id() + report: str = f"Placing {order_type} {side} order for size {size} at price {price} on market {self.market_name} with ID {client_id}." self.logger.info(report) self.reporter(report) signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) - place_instructions = build_place_perp_order_instructions( - self.context, self.wallet, self.perp_market.group, self.margin_account, self.perp_market, price, size, client_order_id, side, order_type) - all_instructions = signers + place_instructions - all_instructions.execute(self.context) - - return Order(id=0, side=side, price=price, size=size, client_id=client_order_id, owner=self.margin_account.address) + place = self.market_instruction_builder.build_place_order_instructions( + side, order_type, price, size, client_id) + (signers + place).execute(self.context) + return Order(id=0, side=side, price=price, size=size, client_id=client_id, owner=self.account.address) def load_orders(self) -> typing.Sequence[Order]: bids_address: PublicKey = self.perp_market.bids @@ -88,7 +82,7 @@ class PerpMarketOperations(MarketOperations): orders = self.load_orders() mine = [] for order in orders: - if order.owner == self.margin_account.address: + if order.owner == self.account.address: mine += [order] return mine diff --git a/mango/serummarketoperations.py b/mango/serummarketoperations.py index 077b030..b28ec84 100644 --- a/mango/serummarketoperations.py +++ b/mango/serummarketoperations.py @@ -14,19 +14,17 @@ # [Email](mailto:hello@blockworks.foundation) -import pyserum.enums import typing from decimal import Decimal from pyserum.market import Market -from solana.rpc.types import TxOpts +from .combinableinstructions import CombinableInstructions from .context import Context from .marketoperations import MarketOperations -from .openorders import OpenOrders from .orders import Order, OrderType, Side from .serummarket import SerumMarket -from .tokenaccount import TokenAccount +from .serummarketinstructionbuilder import SerumMarketInstructionBuilder from .wallet import Wallet @@ -36,17 +34,13 @@ from .wallet import Wallet # class SerumMarketOperations(MarketOperations): - def __init__(self, context: Context, wallet: Wallet, serum_market: SerumMarket, reporter: typing.Callable[[str], None] = None): + def __init__(self, context: Context, wallet: Wallet, serum_market: SerumMarket, market_instruction_builder: SerumMarketInstructionBuilder, reporter: typing.Callable[[str], None] = None): super().__init__() self.context: Context = context self.wallet: Wallet = wallet self.serum_market: SerumMarket = serum_market self.market: Market = Market.load(context.client, serum_market.address, context.dex_program_id) - all_open_orders = OpenOrders.load_for_market_and_owner( - context, serum_market.address, wallet.address, context.dex_program_id, serum_market.base.decimals, serum_market.quote.decimals) - if len(all_open_orders) == 0: - raise Exception(f"No OpenOrders account available for market {serum_market}.") - self.open_orders = all_open_orders[0] + self.market_instruction_builder: SerumMarketInstructionBuilder = market_instruction_builder def report(text): self.logger.info(text) @@ -61,34 +55,22 @@ class SerumMarketOperations(MarketOperations): self.reporter = just_log def cancel_order(self, order: Order) -> typing.Sequence[str]: - self.reporter( - f"Cancelling order {order.id} in openorders {self.open_orders.address} on market {self.serum_market.symbol}.") - try: - response = self.market.cancel_order_by_client_id( - self.wallet.account, self.open_orders.address, order.id, - TxOpts(preflight_commitment=self.context.commitment)) - return [self.context.unwrap_transaction_id_or_raise_exception(response)] - except Exception as exception: - self.logger.warning(f"Failed to cancel order {order.id} - continuing. {exception}") - return [""] + self.reporter(f"Cancelling order {order.id} on market {self.serum_market.symbol}.") + signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) + cancel = self.market_instruction_builder.build_cancel_order_instructions(order) + return (signers + cancel).execute_and_unwrap_transaction_ids(self.context) def place_order(self, side: Side, order_type: OrderType, price: Decimal, size: Decimal) -> Order: client_id: int = self.context.random_client_id() report: str = f"Placing {order_type} {side} order for size {size} at price {price} on market {self.serum_market.symbol} with ID {client_id}." self.logger.info(report) self.reporter(report) - serum_order_type = pyserum.enums.OrderType.POST_ONLY if order_type == OrderType.POST_ONLY else pyserum.enums.OrderType.IOC if order_type == OrderType.IOC else pyserum.enums.OrderType.LIMIT - serum_side = pyserum.enums.Side.BUY if side == Side.BUY else pyserum.enums.Side.SELL - payer_token = self.serum_market.quote if side == Side.BUY else self.serum_market.base - token_account = TokenAccount.fetch_largest_for_owner_and_token(self.context, self.wallet.address, payer_token) - if token_account is None: - raise Exception(f"Could not find payer token account for token {payer_token.symbol}.") - response = self.market.place_order(token_account.address, self.wallet.account, - serum_order_type, serum_side, float(price), float(size), - client_id, TxOpts(preflight_commitment=self.context.commitment)) - self.context.unwrap_or_raise_exception(response) - return Order(id=0, side=side, price=price, size=size, client_id=client_id, owner=self.open_orders.address) + signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) + place = self.market_instruction_builder.build_place_order_instructions( + side, order_type, price, size, client_id) + (signers + place).execute(self.context) + return Order(id=0, side=side, price=price, size=size, client_id=client_id, owner=self.market_instruction_builder.open_orders.address) def load_orders(self) -> typing.Sequence[Order]: asks = self.market.load_asks() diff --git a/mango/spotmarketinstructionbuilder.py b/mango/spotmarketinstructionbuilder.py index eaea26d..db7d1cc 100644 --- a/mango/spotmarketinstructionbuilder.py +++ b/mango/spotmarketinstructionbuilder.py @@ -75,12 +75,7 @@ class SpotMarketInstructionBuilder(MarketInstructionBuilder): if quote_token_account is None: raise Exception(f"Could not find source token account for quote token {spot_market.quote.symbol}.") - market_index: int = -1 - for index, spot in enumerate(group.spot_markets): - if spot is not None and spot.address == spot_market.address: - market_index = index - if market_index == -1: - raise Exception(f"Could not find spot market {spot_market.address} in group {group.address}") + market_index = group.find_spot_market_index(spot_market.address) return SpotMarketInstructionBuilder(context, wallet, group, account, spot_market, raw_market, base_token_account, quote_token_account, market_index, fee_discount_token_address) diff --git a/mango/spotmarketoperations.py b/mango/spotmarketoperations.py index af17f3a..998795a 100644 --- a/mango/spotmarketoperations.py +++ b/mango/spotmarketoperations.py @@ -18,21 +18,18 @@ import itertools import typing from decimal import Decimal -from pyserum.market import Market from pyserum.market.orderbook import OrderBook as SerumOrderBook from pyserum.market.types import Order as SerumOrder -from solana.publickey import PublicKey from .account import Account from .accountinfo import AccountInfo from .combinableinstructions import CombinableInstructions from .context import Context from .group import Group -from .instructions import build_compound_spot_place_order_instructions, build_cancel_spot_order_instructions from .marketoperations import MarketOperations from .orders import Order, OrderType, Side from .spotmarket import SpotMarket -from .tokenaccount import TokenAccount +from .spotmarketinstructionbuilder import SpotMarketInstructionBuilder from .wallet import Wallet @@ -42,25 +39,17 @@ from .wallet import Wallet # class SpotMarketOperations(MarketOperations): - def __init__(self, context: Context, wallet: Wallet, group: Group, account: Account, spot_market: SpotMarket, reporter: typing.Callable[[str], None] = None): + def __init__(self, context: Context, wallet: Wallet, group: Group, account: Account, spot_market: SpotMarket, market_instruction_builder: SpotMarketInstructionBuilder, reporter: typing.Callable[[str], None] = None): super().__init__() self.context: Context = context self.wallet: Wallet = wallet self.group: Group = group self.account: Account = account self.spot_market: SpotMarket = spot_market - self.market: Market = Market.load(context.client, spot_market.address, context.dex_program_id) - self._serum_fee_discount_token_address: typing.Optional[PublicKey] = None - self._serum_fee_discount_token_address_loaded: bool = False + self.market_instruction_builder: SpotMarketInstructionBuilder = market_instruction_builder - market_index: int = -1 - for index, spot in enumerate(self.group.spot_markets): - if spot is not None and spot.address == self.spot_market.address: - market_index = index - if market_index == -1: - raise Exception(f"Could not find spot market {self.spot_market.address} in group {self.group.address}") - - self.group_market_index: int = market_index + self.market_index = group.find_spot_market_index(spot_market.address) + self.open_orders = self.account.spot_open_orders[self.market_index] def report(text): self.logger.info(text) @@ -74,66 +63,31 @@ class SpotMarketOperations(MarketOperations): else: self.reporter = just_log - @property - def serum_fee_discount_token_address(self) -> typing.Optional[PublicKey]: - if self._serum_fee_discount_token_address_loaded: - return self._serum_fee_discount_token_address - - # SRM is always the token Serum uses for fee discounts - token = self.context.token_lookup.find_by_symbol("SRM") - if token is None: - self._serum_fee_discount_token_address_loaded = True - self._serum_fee_discount_token_address = None - return self._serum_fee_discount_token_address - - fee_discount_token_account = TokenAccount.fetch_largest_for_owner_and_token( - self.context, self.wallet.address, token) - if fee_discount_token_account is not None: - self._serum_fee_discount_token_address = fee_discount_token_account.address - - self._serum_fee_discount_token_address_loaded = True - return self._serum_fee_discount_token_address - def cancel_order(self, order: Order) -> typing.Sequence[str]: - report = f"Cancelling order {order.id} on market {self.spot_market.symbol}." - self.logger.info(report) - self.reporter(report) - - open_orders = self.account.spot_open_orders[self.group_market_index] - + self.reporter(f"Cancelling order {order.id} on market {self.spot_market.symbol}.") signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) - cancel_instructions = build_cancel_spot_order_instructions( - self.context, self.wallet, self.group, self.account, self.market, order, open_orders) - all_instructions = signers + cancel_instructions - return all_instructions.execute_and_unwrap_transaction_ids(self.context) + cancel = self.market_instruction_builder.build_cancel_order_instructions(order) + return (signers + cancel).execute_and_unwrap_transaction_ids(self.context) def place_order(self, side: Side, order_type: OrderType, price: Decimal, size: Decimal) -> Order: - payer_token = self.spot_market.quote if side == Side.BUY else self.spot_market.base - payer_token_account = TokenAccount.fetch_largest_for_owner_and_token( - self.context, self.wallet.address, payer_token) - if payer_token_account is None: - raise Exception(f"Could not find a source token account for token {payer_token}.") - - client_order_id = self.context.random_client_id() - report = f"Placing {order_type} {side} order for size {size} at price {price} on market {self.spot_market.symbol} using client ID {client_order_id}." + client_id: int = self.context.random_client_id() + report: str = f"Placing {order_type} {side} order for size {size} at price {price} on market {self.spot_market.symbol} with ID {client_id}." self.logger.info(report) self.reporter(report) signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) - place_instructions = build_compound_spot_place_order_instructions( - self.context, self.wallet, self.group, self.account, self.market, payer_token_account.address, - order_type, side, price, size, client_order_id, self.serum_fee_discount_token_address) + place = self.market_instruction_builder.build_place_order_instructions( + side, order_type, price, size, client_id) + (signers + place).execute(self.context) - all_instructions = signers + place_instructions - all_instructions.execute(self.context) - - return Order(id=0, side=side, price=price, size=size, client_id=client_order_id, owner=self.account.address) + return Order(id=0, side=side, price=price, size=size, client_id=client_id, owner=self.open_orders) def _load_serum_orders(self) -> typing.Sequence[SerumOrder]: + raw_market = self.market_instruction_builder.raw_market [bids_info, asks_info] = AccountInfo.load_multiple( - self.context, [self.market.state.bids(), self.market.state.asks()]) - bids_orderbook = SerumOrderBook.from_bytes(self.market.state, bids_info.data) - asks_orderbook = SerumOrderBook.from_bytes(self.market.state, asks_info.data) + self.context, [raw_market.state.bids(), raw_market.state.asks()]) + bids_orderbook = SerumOrderBook.from_bytes(raw_market.state, bids_info.data) + asks_orderbook = SerumOrderBook.from_bytes(raw_market.state, asks_info.data) return list(itertools.chain(bids_orderbook.orders(), asks_orderbook.orders())) @@ -146,12 +100,11 @@ class SpotMarketOperations(MarketOperations): return orders def load_my_orders(self) -> typing.Sequence[Order]: - open_orders_account = self.account.spot_open_orders[self.group_market_index] - if not open_orders_account: + if not self.open_orders: return [] all_orders = self._load_serum_orders() - serum_orders = [o for o in all_orders if o.open_order_address == open_orders_account] + serum_orders = [o for o in all_orders if o.open_order_address == self.open_orders] orders: typing.List[Order] = [] for serum_order in serum_orders: orders += [Order.from_serum_order(serum_order)]