From 3e4ba1052c89ac4b66d7f1d0ed9a6a6132876f88 Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Wed, 30 Jun 2021 14:08:37 +0100 Subject: [PATCH] Initial work on placing/cancelling spot market orders. --- mango/__init__.py | 2 + mango/account.py | 4 +- mango/context.py | 15 +- mango/createmarketoperations.py | 13 +- mango/idsjsonmarketlookup.py | 50 +++++-- mango/instructions.py | 199 ++++++++++++++++++++++++++- mango/layouts/layouts.py | 65 ++++++++- mango/market.py | 2 +- mango/openorders.py | 4 +- mango/orders.py | 11 ++ mango/perpsmarket.py | 38 +++++ mango/serummarket.py | 38 +++++ mango/serummarketlookup.py | 14 +- mango/serummarketoperations.py | 35 ++--- mango/spotmarket.py | 3 +- mango/spotmarketoperations.py | 168 ++++++++++++++++++++++ scripts/worlds-simplest-market-maker | 6 +- tests/fakes.py | 4 +- tests/test_liquidationevent.py | 15 +- tests/test_spotmarket.py | 14 +- 20 files changed, 622 insertions(+), 78 deletions(-) create mode 100644 mango/perpsmarket.py create mode 100644 mango/serummarket.py create mode 100644 mango/spotmarketoperations.py diff --git a/mango/__init__.py b/mango/__init__.py index 039be5c..1fe1ffd 100644 --- a/mango/__init__.py +++ b/mango/__init__.py @@ -32,8 +32,10 @@ from .oraclefactory import create_oracle_provider from .perpmarket import PerpMarket from .perpmarketinfo import PerpMarketInfo from .perpmarketoperations import PerpMarketOperations +from .perpsmarket import PerpsMarket from .retrier import RetryWithPauses, retry_context from .rootbank import NodeBank, RootBank +from .serummarket import SerumMarket from .serummarketlookup import SerumMarketLookup from .serummarketoperations import SerumMarketOperations from .spltokenlookup import SplTokenLookup diff --git a/mango/account.py b/mango/account.py index 709a7e0..c030d0b 100644 --- a/mango/account.py +++ b/mango/account.py @@ -114,8 +114,8 @@ class Account(AddressableAccount): borrows = ", ".join([f"{borrow}" for borrow in self.borrows]) spot_open_orders = ", ".join([f"{oo}" for oo in self.spot_open_orders if oo is not None]) perp_accounts = ", ".join( - [f"{perp}" for perp in self.perp_accounts if perp.open_orders.is_free_bits != 0xFFFFFFFF]) - return f"""Β« π™ΌπšŽπš›πš™πšœπ™°πšŒπšŒπš˜πšžπš—πš {self.version} [{self.address}] + [f"{perp}".replace("\n", "\n ") for perp in self.perp_accounts if perp.open_orders.is_free_bits != 0xFFFFFFFF]) + return f"""Β« π™°πšŒπšŒπš˜πšžπš—πš {self.version} [{self.address}] {self.meta_data} Owner: {self.owner} Group: {self.group} diff --git a/mango/context.py b/mango/context.py index 3666cb5..b3a7ae3 100644 --- a/mango/context.py +++ b/mango/context.py @@ -50,7 +50,7 @@ from .tokenlookup import TokenLookup, CompoundTokenLookup # * GROUP_NAME (defaults to: BTC_ETH_USDT) # -_default_group_data = MangoConstants["groups"][2] +_default_group_data = MangoConstants["groups"][0] default_cluster = os.environ.get("CLUSTER") or _default_group_data["cluster"] default_cluster_url = os.environ.get("CLUSTER_URL") or MangoConstants["cluster_urls"][default_cluster] @@ -84,14 +84,17 @@ class Context: self.transaction_options: TxOpts = TxOpts(preflight_commitment=self.commitment) self.encoding: str = "base64" ids_json_token_lookup: TokenLookup = IdsJsonTokenLookup(cluster, group_name) - spl_token_lookup: TokenLookup = SplTokenLookup.load(token_filename) - all_token_lookup: TokenLookup = CompoundTokenLookup( - [ids_json_token_lookup, spl_token_lookup]) + all_token_lookup = ids_json_token_lookup + if cluster == "mainnet-beta": + spl_token_lookup: TokenLookup = SplTokenLookup.load(token_filename) + all_token_lookup = CompoundTokenLookup([ids_json_token_lookup, spl_token_lookup]) self.token_lookup: TokenLookup = all_token_lookup ids_json_market_lookup: MarketLookup = IdsJsonMarketLookup(cluster) - serum_market_lookup: SerumMarketLookup = SerumMarketLookup.load(token_filename) - all_market_lookup = CompoundMarketLookup([ids_json_market_lookup, serum_market_lookup]) + all_market_lookup = ids_json_market_lookup + if cluster == "mainnet-beta": + serum_market_lookup: SerumMarketLookup = SerumMarketLookup.load(token_filename) + all_market_lookup = CompoundMarketLookup([ids_json_market_lookup, serum_market_lookup]) self.market_lookup: MarketLookup = all_market_lookup # kangda said in Discord: https://discord.com/channels/791995070613159966/836239696467591186/847816026245693451 diff --git a/mango/createmarketoperations.py b/mango/createmarketoperations.py index 1bea980..c086255 100644 --- a/mango/createmarketoperations.py +++ b/mango/createmarketoperations.py @@ -23,8 +23,11 @@ from .market import Market from .marketoperations import MarketOperations, NullMarketOperations from .perpmarket import PerpMarket from .perpmarketoperations import PerpMarketOperations -# from .serummarketoperations import SerumMarketOperations +from .perpsmarket import PerpsMarket +from .serummarket import SerumMarket +from .serummarketoperations import SerumMarketOperations from .spotmarket import SpotMarket +from .spotmarketoperations import SpotMarketOperations from .wallet import Wallet # # πŸ₯­ create_market_operations @@ -35,9 +38,13 @@ from .wallet import Wallet def create_market_operations(context: Context, wallet: Wallet, dry_run: bool, market: Market, reporter: typing.Callable[[str], None]) -> MarketOperations: if dry_run: return NullMarketOperations(market.symbol, reporter) + elif isinstance(market, SerumMarket): + return SerumMarketOperations(context, wallet, market, reporter) elif isinstance(market, SpotMarket): - # return SerumMarketOperations(context, wallet, market, reporter) - # elif isinstance(market, PerpMarket): + 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) + elif isinstance(market, PerpsMarket): group = Group.load(context, context.group_id) margin_accounts = Account.load_all_for_owner(context, wallet.address, group) perp_market_info = group.perp_markets[0] diff --git a/mango/idsjsonmarketlookup.py b/mango/idsjsonmarketlookup.py index 6b91cb6..2bb7851 100644 --- a/mango/idsjsonmarketlookup.py +++ b/mango/idsjsonmarketlookup.py @@ -13,7 +13,7 @@ # [Github](https://github.com/blockworks-foundation) # [Email](mailto:hello@blockworks.foundation) - +import enum import typing from decimal import Decimal @@ -22,10 +22,22 @@ from solana.publickey import PublicKey from .constants import MangoConstants from .market import Market from .marketlookup import MarketLookup +from .perpsmarket import PerpsMarket from .spotmarket import SpotMarket from .token import Token +class IdsJsonMarketType(enum.Enum): + PERP = enum.auto() + SPOT = enum.auto() + + def __str__(self) -> str: + return self.name + + def __repr__(self) -> str: + return f"{self}" + + # # πŸ₯­ IdsJsonMarketLookup class # # This class allows us to look up market data from our ids.json configuration file. @@ -38,7 +50,7 @@ class IdsJsonMarketLookup(MarketLookup): self.cluster: str = cluster @staticmethod - def _from_dict(data: typing.Dict, tokens: typing.Sequence[Token], quote_symbol: str) -> Market: + def _from_dict(market_type: IdsJsonMarketType, group_address: PublicKey, data: typing.Dict, tokens: typing.Sequence[Token], quote_symbol: str) -> Market: base_symbol = data["baseSymbol"] if tokens[0].symbol == quote_symbol and tokens[1].symbol == base_symbol: quote = tokens[0] @@ -50,7 +62,10 @@ class IdsJsonMarketLookup(MarketLookup): raise Exception(f"Could not find base ('{base_symbol}') or quote symbol ('{quote_symbol}') in tokens.") address = PublicKey(data["publicKey"]) - return SpotMarket(base, quote, address) + if market_type == IdsJsonMarketType.PERP: + return PerpsMarket(base, quote, address) + else: + return SpotMarket(base, quote, address, group_address) @staticmethod def _load_tokens(data: typing.Dict) -> typing.Sequence[Token]: @@ -64,28 +79,45 @@ class IdsJsonMarketLookup(MarketLookup): def find_by_symbol(self, symbol: str) -> typing.Optional[Market]: for group in MangoConstants["groups"]: if group["cluster"] == self.cluster: + group_address: PublicKey = PublicKey(group["publicKey"]) for market_data in group["perpMarkets"]: if market_data["name"].upper() == symbol.upper(): tokens = IdsJsonMarketLookup._load_tokens(group["tokens"]) - return IdsJsonMarketLookup._from_dict(market_data, tokens, group["quoteSymbol"]) + return IdsJsonMarketLookup._from_dict(IdsJsonMarketType.PERP, group_address, market_data, tokens, group["quoteSymbol"]) + for market_data in group["spotMarkets"]: + if market_data["name"].upper() == symbol.upper(): + tokens = IdsJsonMarketLookup._load_tokens(group["tokens"]) + return IdsJsonMarketLookup._from_dict(IdsJsonMarketType.SPOT, group_address, market_data, tokens, group["quoteSymbol"]) return None def find_by_address(self, address: PublicKey) -> typing.Optional[Market]: for group in MangoConstants["groups"]: if group["cluster"] == self.cluster: - for spot_market in group["spotMarkets"]: - if spot_market["key"] == str(address): + group_address: PublicKey = PublicKey(group["publicKey"]) + for market_data in group["perpMarkets"]: + if market_data["key"] == str(address): tokens = IdsJsonMarketLookup._load_tokens(group["tokens"]) - return IdsJsonMarketLookup._from_dict(spot_market, tokens, group["quoteSymbol"]) + return IdsJsonMarketLookup._from_dict(IdsJsonMarketType.PERP, group_address, market_data, tokens, group["quoteSymbol"]) + for market_data in group["spotMarkets"]: + if market_data["key"] == str(address): + tokens = IdsJsonMarketLookup._load_tokens(group["tokens"]) + return IdsJsonMarketLookup._from_dict(IdsJsonMarketType.SPOT, group_address, market_data, tokens, group["quoteSymbol"]) return None def all_markets(self) -> typing.Sequence[Market]: markets = [] for group in MangoConstants["groups"]: if group["cluster"] == self.cluster: - for spot_market in group["spotMarkets"]: + group_address: PublicKey = PublicKey(group["publicKey"]) + for market_data in group["perpMarkets"]: tokens = IdsJsonMarketLookup._load_tokens(group["tokens"]) - market = IdsJsonMarketLookup._from_dict(spot_market, tokens, group["quoteSymbol"]) + market = IdsJsonMarketLookup._from_dict( + IdsJsonMarketType.PERP, group_address, market_data, tokens, group["quoteSymbol"]) + markets = [market] + for market_data in group["spotMarkets"]: + tokens = IdsJsonMarketLookup._load_tokens(group["tokens"]) + market = IdsJsonMarketLookup._from_dict( + IdsJsonMarketType.SPOT, group_address, market_data, tokens, group["quoteSymbol"]) markets = [market] return markets diff --git a/mango/instructions.py b/mango/instructions.py index cb353fd..8265714 100644 --- a/mango/instructions.py +++ b/mango/instructions.py @@ -14,6 +14,7 @@ # [Email](mailto:hello@blockworks.foundation) +import pyserum.enums import typing from decimal import Decimal @@ -24,7 +25,7 @@ from pyserum.open_orders_account import make_create_account_instruction from solana.account import Account as SolanaAccount from solana.publickey import PublicKey from solana.system_program import CreateAccountParams, create_account -from solana.sysvar import SYSVAR_CLOCK_PUBKEY +from solana.sysvar import SYSVAR_CLOCK_PUBKEY, SYSVAR_RENT_PUBKEY from solana.transaction import AccountMeta, TransactionInstruction from spl.token.constants import ACCOUNT_LEN, TOKEN_PROGRAM_ID from spl.token.instructions import CloseAccountParams, InitializeAccountParams, Transfer2Params, close_account, initialize_account, transfer2 @@ -145,6 +146,7 @@ def build_serum_consume_events_instructions(context: Context, wallet: Wallet, ma market=market.state.public_key(), event_queue=market.state.event_queue(), open_orders_accounts=open_orders_addresses, + program_id=context.dex_program_id, limit=limit )) @@ -369,9 +371,202 @@ def build_withdraw_instructions(context: Context, wallet: Wallet, group: Group, # pubkey=oo_address or SYSTEM_PROGRAM_ADDRESS) for oo_address in margin_account.spot_open_orders]) ], program_id=context.program_id, - data=layouts.WITHDRAW_V3.build({ + data=layouts.WITHDRAW.build({ "quantity": value, "allow_borrow": allow_borrow }) ) return [withdraw] + + +# # πŸ₯­ build_mango_place_order_instructions function +# +# Creates a Mango order-placing instruction using the Serum instruction as the inner instruction. +# + +def build_spot_place_order_instructions(context: Context, wallet: Wallet, group: Group, account: Account, + market: Market, source: PublicKey, open_orders_address: PublicKey, + order_type: OrderType, side: Side, price: Decimal, + quantity: Decimal, client_id: int, + fee_discount_address: typing.Optional[PublicKey]) -> typing.Sequence[TransactionInstruction]: + + 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 + intrinsic_price = market.state.price_number_to_lots(float(price)) + max_base_quantity = market.state.base_size_number_to_lots(float(quantity)) + max_quote_quantity = market.state.base_size_number_to_lots( + float(quantity)) * market.state.quote_lot_size() * market.state.price_number_to_lots(float(price)) + + quote_token_info = group.shared_quote_token + base_token_infos = [ + token_info for token_info in group.base_tokens if token_info is not None and token_info.token.mint == market.state.base_mint()] + if len(base_token_infos) != 1: + raise Exception( + f"Could not find base token info for group {group.address} - length was {len(base_token_infos)} when it should be 1.") + base_token_info = base_token_infos[0] + + vault_signer = PublicKey.create_program_address( + [bytes(market.state.public_key()), market.state.vault_signer_nonce().to_bytes(8, byteorder="little")], + market.state.program_id(), + ) + + base_root_bank = RootBank.load(context, base_token_info.root_bank) + base_node_bank = base_root_bank.pick_node_bank(context) + quote_root_bank = RootBank.load(context, quote_token_info.root_bank) + quote_node_bank = quote_root_bank.pick_node_bank(context) + + # /// Accounts expected by this instruction (22+openorders): + # { isSigner: false, isWritable: false, pubkey: mangoGroupPk }, + # { isSigner: false, isWritable: true, pubkey: mangoAccountPk }, + # { isSigner: true, isWritable: false, pubkey: ownerPk }, + # { isSigner: false, isWritable: false, pubkey: mangoCachePk }, + # { isSigner: false, isWritable: false, pubkey: serumDexPk }, + # { isSigner: false, isWritable: true, pubkey: spotMarketPk }, + # { isSigner: false, isWritable: true, pubkey: bidsPk }, + # { isSigner: false, isWritable: true, pubkey: asksPk }, + # { isSigner: false, isWritable: true, pubkey: requestQueuePk }, + # { isSigner: false, isWritable: true, pubkey: eventQueuePk }, + # { isSigner: false, isWritable: true, pubkey: spotMktBaseVaultPk }, + # { isSigner: false, isWritable: true, pubkey: spotMktQuoteVaultPk }, + # { isSigner: false, isWritable: false, pubkey: baseRootBankPk }, + # { isSigner: false, isWritable: true, pubkey: baseNodeBankPk }, + # { isSigner: false, isWritable: true, pubkey: quoteRootBankPk }, + # { isSigner: false, isWritable: true, pubkey: quoteNodeBankPk }, + # { isSigner: false, isWritable: true, pubkey: quoteVaultPk }, + # { isSigner: false, isWritable: true, pubkey: baseVaultPk }, + # { isSigner: false, isWritable: false, pubkey: TOKEN_PROGRAM_ID }, + # { isSigner: false, isWritable: false, pubkey: signerPk }, + # { isSigner: false, isWritable: false, pubkey: SYSVAR_RENT_PUBKEY }, + # { isSigner: false, isWritable: false, pubkey: dexSignerPk }, + # ...openOrders.map((pubkey) => ({ + # isSigner: false, + # isWritable: true, // TODO: only pass the one writable you are going to place the order on + # pubkey, + # })), + fee_discount_address_meta: typing.List[AccountMeta] = [] + if fee_discount_address is not None: + fee_discount_address_meta = [AccountMeta(is_signer=False, is_writable=False, pubkey=fee_discount_address)] + instructions = [ + TransactionInstruction( + keys=[ + 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=group.cache), + AccountMeta(is_signer=False, is_writable=False, pubkey=context.dex_program_id), + 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=market.state.request_queue()), + AccountMeta(is_signer=False, is_writable=True, pubkey=market.state.event_queue()), + 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_root_bank.address), + AccountMeta(is_signer=False, is_writable=True, pubkey=base_node_bank.address), + AccountMeta(is_signer=False, is_writable=True, 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=True, pubkey=base_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=SYSVAR_RENT_PUBKEY), + AccountMeta(is_signer=False, is_writable=False, pubkey=vault_signer), + *list([AccountMeta(is_signer=False, is_writable=(oo_address == open_orders_address), + pubkey=oo_address or SYSTEM_PROGRAM_ADDRESS) for oo_address in account.spot_open_orders]), + *fee_discount_address_meta + ], + program_id=context.program_id, + data=layouts.PLACE_SPOT_ORDER.build( + dict( + side=serum_side, + limit_price=intrinsic_price, + max_base_quantity=max_base_quantity, + max_quote_quantity=max_quote_quantity, + self_trade_behavior=pyserum.enums.SelfTradeBehavior.DECREMENT_TAKE, + order_type=serum_order_type, + client_id=client_id, + limit=65535, + ) + ) + ) + ] + + return instructions + + +def build_compound_spot_place_order_instructions(context: Context, wallet: Wallet, group: Group, account: Account, + market: Market, source: PublicKey, open_orders_address: PublicKey, + order_type: OrderType, side: Side, price: Decimal, + quantity: Decimal, client_id: int, + fee_discount_address: typing.Optional[PublicKey]) -> typing.Sequence[TransactionInstruction]: + place_order = build_spot_place_order_instructions( + context, wallet, group, account, market, source, open_orders_address, order_type, side, price, quantity, client_id, fee_discount_address) + open_order_accounts = market.find_open_orders_accounts_for_owner(wallet.address) + open_order_addresses = list(oo.address for oo in open_order_accounts) + consume_events = build_serum_consume_events_instructions(context, wallet, market, open_order_addresses) + + quote_token_info = group.shared_quote_token + base_token_infos = [ + token_info for token_info in group.base_tokens if token_info is not None and token_info.token.mint == market.state.base_mint()] + if len(base_token_infos) != 1: + raise Exception( + f"Could not find base token info for group {group.address} - length was {len(base_token_infos)} when it should be 1.") + base_token_info = base_token_infos[0] + base_token_account = TokenAccount.fetch_largest_for_owner_and_token(context, wallet.address, base_token_info.token) + quote_token_account = TokenAccount.fetch_largest_for_owner_and_token( + context, wallet.address, quote_token_info.token) + + settle: typing.Sequence[TransactionInstruction] = [] + if base_token_account is not None and quote_token_account is not None: + settlement_open_orders = [oo for oo in open_order_accounts if oo.market == market.state.public_key()] + if len(settlement_open_orders) > 0 and settlement_open_orders[0] is not None: + settle = build_serum_settle_instructions( + context, wallet, market, settlement_open_orders[0].address, base_token_account.address, quote_token_account.address) + + return [*place_order, *consume_events, *settle] + + +# # πŸ₯­ build_cancel_perp_order_instruction function +# +# Builds the instructions necessary for cancelling a perp order. +# + + +def build_cancel_spot_order_instructions(context: Context, wallet: Wallet, group: Group, account: Account, market: Market, order: Order, open_orders_address: PublicKey) -> typing.Sequence[TransactionInstruction]: + # { buy: 0, sell: 1 } + raw_side: int = 1 if order.side == Side.SELL else 0 + + # Accounts expected by this instruction: + # { isSigner: false, isWritable: false, pubkey: mangoGroupPk }, + # { isSigner: true, isWritable: false, pubkey: ownerPk }, + # { isSigner: false, isWritable: false, pubkey: mangoAccountPk }, + # { isSigner: false, isWritable: false, pubkey: dexProgramId }, + # { isSigner: false, isWritable: true, pubkey: spotMarketPk }, + # { isSigner: false, isWritable: true, pubkey: bidsPk }, + # { isSigner: false, isWritable: true, pubkey: asksPk }, + # { isSigner: false, isWritable: true, pubkey: openOrdersPk }, + # { isSigner: false, isWritable: false, pubkey: signerKey }, + # { isSigner: false, isWritable: true, pubkey: eventQueuePk }, + + return [ + TransactionInstruction( + keys=[ + 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.dex_program_id), + 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.program_id, + data=layouts.CANCEL_SPOT_ORDER.build( + { + "order_id": order.id, + "side": raw_side + }) + ) + ] diff --git a/mango/layouts/layouts.py b/mango/layouts/layouts.py index 45f96be8..35217ab 100644 --- a/mango/layouts/layouts.py +++ b/mango/layouts/layouts.py @@ -600,7 +600,7 @@ CANCEL_PERP_ORDER = construct.Struct( # quantity: u64, # allow_borrow: bool, # }, -WITHDRAW_V3 = construct.Struct( +WITHDRAW = construct.Struct( "variant" / construct.Const(14, construct.BytesInteger(4, swapped=True)), "quantity" / DecimalAdapter(), @@ -608,17 +608,74 @@ WITHDRAW_V3 = construct.Struct( ) +# /// Place an order on the Serum Dex using Mango account +# /// Accounts expected by this instruction (22+openorders): +# { isSigner: false, isWritable: false, pubkey: mangoGroupPk }, +# { isSigner: false, isWritable: true, pubkey: mangoAccountPk }, +# { isSigner: true, isWritable: false, pubkey: ownerPk }, +# { isSigner: false, isWritable: false, pubkey: mangoCachePk }, +# { isSigner: false, isWritable: false, pubkey: serumDexPk }, +# { isSigner: false, isWritable: true, pubkey: spotMarketPk }, +# { isSigner: false, isWritable: true, pubkey: bidsPk }, +# { isSigner: false, isWritable: true, pubkey: asksPk }, +# { isSigner: false, isWritable: true, pubkey: requestQueuePk }, +# { isSigner: false, isWritable: true, pubkey: eventQueuePk }, +# { isSigner: false, isWritable: true, pubkey: spotMktBaseVaultPk }, +# { isSigner: false, isWritable: true, pubkey: spotMktQuoteVaultPk }, +# { isSigner: false, isWritable: false, pubkey: baseRootBankPk }, +# { isSigner: false, isWritable: true, pubkey: baseNodeBankPk }, +# { isSigner: false, isWritable: true, pubkey: quoteRootBankPk }, +# { isSigner: false, isWritable: true, pubkey: quoteNodeBankPk }, +# { isSigner: false, isWritable: true, pubkey: quoteVaultPk }, +# { isSigner: false, isWritable: true, pubkey: baseVaultPk }, +# { isSigner: false, isWritable: false, pubkey: TOKEN_PROGRAM_ID }, +# { isSigner: false, isWritable: false, pubkey: signerPk }, +# { isSigner: false, isWritable: false, pubkey: SYSVAR_RENT_PUBKEY }, +# { isSigner: false, isWritable: false, pubkey: dexSignerPk }, +# ...openOrders.map((pubkey) => ({ +# isSigner: false, +# isWritable: true, // TODO: only pass the one writable you are going to place the order on +# pubkey, +# })), +PLACE_SPOT_ORDER = construct.Struct( + "variant" / construct.Const(9, construct.BytesInteger(4, swapped=True)), # 4 + + '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 +) + +# /// Cancel an order using dex instruction +# /// +# /// Accounts expected by this instruction (): +# /// +# CancelSpotOrder { +# order: serum_dex::instruction::CancelOrderInstructionV2, +# }, +CANCEL_SPOT_ORDER = construct.Struct( + "variant" / construct.Const(20, construct.BytesInteger(4, swapped=True)), + + 'side' / DecimalAdapter(4), + "order_id" / DecimalAdapter(16) +) + + InstructionParsersByVariant = { 0: None, # INIT_MANGO_GROUP, 1: INIT_MANGO_ACCOUNT, # INIT_MANGO_ACCOUNT, 2: None, # DEPOSIT, - 3: WITHDRAW_V3, # WITHDRAW, + 3: WITHDRAW, # WITHDRAW, 4: None, # ADD_SPOT_MARKET, 5: None, # ADD_TO_BASKET, 6: None, # BORROW, 7: None, # CACHE_PRICES, 8: None, # CACHE_ROOT_BANKS, - 9: None, # PLACE_SPOT_ORDER, + 9: PLACE_SPOT_ORDER, # PLACE_SPOT_ORDER, 10: None, # ADD_ORACLE, 11: None, # ADD_PERP_MARKET, 12: PLACE_PERP_ORDER, # PLACE_PERP_ORDER, @@ -629,7 +686,7 @@ InstructionParsersByVariant = { 17: None, # UPDATE_FUNDING, 18: None, # SET_ORACLE, 19: None, # SETTLE_FUNDS, - 20: None, # CANCEL_SPOT_ORDER, + 20: CANCEL_SPOT_ORDER, # CANCEL_SPOT_ORDER, 21: None, # UPDATE_ROOT_BANK, 22: None, # SETTLE_PNL, 23: None, # SETTLE_BORROW, diff --git a/mango/market.py b/mango/market.py index f92c528..2763cf3 100644 --- a/mango/market.py +++ b/mango/market.py @@ -36,7 +36,7 @@ class Market(metaclass=abc.ABCMeta): return f"{self.base.symbol}/{self.quote.symbol}" def __str__(self) -> str: - return f"Β« Market {self.symbol} Β»" + return f"Β« π™ΌπšŠπš›πš”πšŽπš {self.symbol} Β»" def __repr__(self) -> str: return f"{self}" diff --git a/mango/openorders.py b/mango/openorders.py index c18e189..a38de85 100644 --- a/mango/openorders.py +++ b/mango/openorders.py @@ -128,8 +128,8 @@ class OpenOrders(AddressableAccount): ) ] - response = context.client.get_program_accounts( - context.dex_program_id, data_size=layouts.OPEN_ORDERS.sizeof(), memcmp_opts=filters, commitment=context.commitment, encoding="base64") + response = context.client.get_program_accounts(program_id, data_size=layouts.OPEN_ORDERS.sizeof( + ), memcmp_opts=filters, commitment=context.commitment, encoding="base64") accounts = list(map(lambda pair: AccountInfo._from_response_values(pair[0], pair[1]), [ (result["account"], PublicKey(result["pubkey"])) for result in response["result"]])) return list(map(lambda acc: OpenOrders.parse(acc, base_decimals, quote_decimals), accounts)) diff --git a/mango/orders.py b/mango/orders.py index 7d57aab..17bc5fc 100644 --- a/mango/orders.py +++ b/mango/orders.py @@ -15,10 +15,12 @@ import enum +import pyserum.enums import typing from decimal import Decimal +from pyserum.market.types import Order as SerumOrder from solana.publickey import PublicKey @@ -79,3 +81,12 @@ class Order(typing.NamedTuple): side: Side price: Decimal size: Decimal + + @staticmethod + def from_serum_order(serum_order: SerumOrder) -> "Order": + price = Decimal(serum_order.info.price) + size = Decimal(serum_order.info.size) + side = Side.BUY if serum_order.side == pyserum.enums.Side.BUY else Side.SELL + order = Order(id=serum_order.order_id, side=side, price=price, size=size, + client_id=serum_order.client_id, owner=serum_order.open_order_address) + return order diff --git a/mango/perpsmarket.py b/mango/perpsmarket.py new file mode 100644 index 0000000..030d8dc --- /dev/null +++ b/mango/perpsmarket.py @@ -0,0 +1,38 @@ +# # ⚠ Warning +# +# 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. +# +# [πŸ₯­ 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) + + +from solana.publickey import PublicKey + +from .market import Market +from .token import Token + + +# # πŸ₯­ PerpsMarket class +# +# This class encapsulates our knowledge of a Mango perps market. +# + + +class PerpsMarket(Market): + def __init__(self, base: Token, quote: Token, address: PublicKey): + super().__init__(base, quote) + self.address: PublicKey = address + + def __str__(self) -> str: + return f"Β« π™ΏπšŽπš›πš™πšœπ™ΌπšŠπš›πš”πšŽπš {self.symbol}: {self.address} Β»" + + def __repr__(self) -> str: + return f"{self}" diff --git a/mango/serummarket.py b/mango/serummarket.py new file mode 100644 index 0000000..8cb24ff --- /dev/null +++ b/mango/serummarket.py @@ -0,0 +1,38 @@ +# # ⚠ Warning +# +# 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. +# +# [πŸ₯­ 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) + + +from solana.publickey import PublicKey + +from .market import Market +from .token import Token + + +# # πŸ₯­ SerumMarket class +# +# This class encapsulates our knowledge of a Serum spot market. +# + + +class SerumMarket(Market): + def __init__(self, base: Token, quote: Token, address: PublicKey): + super().__init__(base, quote) + self.address: PublicKey = address + + def __str__(self) -> str: + return f"Β« πš‚πšŽπš›πšžπš–π™ΌπšŠπš›πš”πšŽπš {self.symbol}: {self.address} Β»" + + def __repr__(self) -> str: + return f"{self}" diff --git a/mango/serummarketlookup.py b/mango/serummarketlookup.py index b55231b..a443601 100644 --- a/mango/serummarketlookup.py +++ b/mango/serummarketlookup.py @@ -22,7 +22,7 @@ from solana.publickey import PublicKey from .market import Market from .marketlookup import MarketLookup -from .spotmarket import SpotMarket +from .serummarket import SerumMarket from .token import Token @@ -119,7 +119,7 @@ class SerumMarketLookup(MarketLookup): f"Could not find market with quote token '{quote.symbol}'. Only markets based on USDC or USDT are supported.") return None - return SpotMarket(base, quote, market_address) + return SerumMarket(base, quote, market_address) def find_by_address(self, address: PublicKey) -> typing.Optional[Market]: address_string: str = str(address) @@ -136,7 +136,7 @@ class SerumMarketLookup(MarketLookup): raise Exception("Could not load token data for USDC (which should always be present).") quote = Token(quote_data["symbol"], quote_data["name"], PublicKey( quote_data["address"]), Decimal(quote_data["decimals"])) - return SpotMarket(base, quote, market_address) + return SerumMarket(base, quote, market_address) if "serumV3Usdt" in token_data["extensions"]: if token_data["extensions"]["serumV3Usdt"] == address_string: market_address_string = token_data["extensions"]["serumV3Usdt"] @@ -148,14 +148,14 @@ class SerumMarketLookup(MarketLookup): raise Exception("Could not load token data for USDT (which should always be present).") quote = Token(quote_data["symbol"], quote_data["name"], PublicKey( quote_data["address"]), Decimal(quote_data["decimals"])) - return SpotMarket(base, quote, market_address) + return SerumMarket(base, quote, market_address) return None def all_markets(self) -> typing.Sequence[Market]: usdt = SerumMarketLookup._find_token_by_symbol_or_error("USDT", self.token_data) usdc = SerumMarketLookup._find_token_by_symbol_or_error("USDC", self.token_data) - all_markets: typing.List[SpotMarket] = [] + all_markets: typing.List[SerumMarket] = [] for token_data in self.token_data["tokens"]: if "extensions" in token_data: if "serumV3Usdc" in token_data["extensions"]: @@ -163,12 +163,12 @@ class SerumMarketLookup(MarketLookup): market_address = PublicKey(market_address_string) base = Token(token_data["symbol"], token_data["name"], PublicKey( token_data["address"]), Decimal(token_data["decimals"])) - all_markets += [SpotMarket(base, usdc, market_address)] + all_markets += [SerumMarket(base, usdc, market_address)] 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"], PublicKey( token_data["address"]), Decimal(token_data["decimals"])) - all_markets += [SpotMarket(base, usdt, market_address)] + all_markets += [SerumMarket(base, usdt, market_address)] return all_markets diff --git a/mango/serummarketoperations.py b/mango/serummarketoperations.py index 420997f..15c39a0 100644 --- a/mango/serummarketoperations.py +++ b/mango/serummarketoperations.py @@ -19,14 +19,13 @@ import typing from decimal import Decimal from pyserum.market import Market -from pyserum.market.types import Order as SerumOrder from solana.rpc.types import TxOpts from .context import Context from .marketoperations import MarketOperations from .openorders import OpenOrders from .orders import Order, OrderType, Side -from .spotmarket import SpotMarket +from .serummarket import SerumMarket from .tokenaccount import TokenAccount from .wallet import Wallet @@ -37,16 +36,16 @@ from .wallet import Wallet # class SerumMarketOperations(MarketOperations): - def __init__(self, context: Context, wallet: Wallet, spot_market: SpotMarket, reporter: typing.Callable[[str], None] = None): + def __init__(self, context: Context, wallet: Wallet, serum_market: SerumMarket, reporter: typing.Callable[[str], None] = None): super().__init__() self.context: Context = context self.wallet: Wallet = wallet - self.spot_market: SpotMarket = spot_market - self.market: Market = Market.load(context.client, spot_market.address, context.dex_program_id) + 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, spot_market.address, wallet.address, context.dex_program_id, spot_market.base.decimals, spot_market.quote.decimals) + 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 {spot_market}.") + raise Exception(f"No OpenOrders account available for market {serum_market}.") self.open_orders = all_open_orders[0] def report(text): @@ -63,7 +62,7 @@ class SerumMarketOperations(MarketOperations): def cancel_order(self, order: Order) -> str: self.reporter( - f"Cancelling order {order.id} in openorders {self.open_orders.address} on market {self.spot_market.symbol}.") + 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, @@ -75,12 +74,12 @@ class SerumMarketOperations(MarketOperations): 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.spot_market.symbol} with ID {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.spot_market.quote if side == Side.BUY else self.spot_market.base + 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}.") @@ -91,23 +90,15 @@ class SerumMarketOperations(MarketOperations): 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) - def _serum_order_to_order(serum_order: SerumOrder) -> Order: - price = Decimal(serum_order.info.price) - size = Decimal(serum_order.info.size) - side = Side.BUY if serum_order.side == pyserum.enums.Side.BUY else Side.SELL - order = Order(id=serum_order.order_id, side=side, price=price, size=size, - client_id=serum_order.client_id, owner=serum_order.open_order_address) - return order - def load_orders(self) -> typing.Sequence[Order]: asks = self.market.load_asks() orders: typing.List[Order] = [] for serum_order in asks: - orders += [SerumMarketOperations._serum_order_to_order(serum_order)] + orders += [Order.from_serum_order(serum_order)] bids = self.market.load_bids() for serum_order in bids: - orders += [SerumMarketOperations._serum_order_to_order(serum_order)] + orders += [Order.from_serum_order(serum_order)] return orders @@ -115,9 +106,9 @@ class SerumMarketOperations(MarketOperations): serum_orders = self.market.load_orders_for_owner(self.wallet.address) orders: typing.List[Order] = [] for serum_order in serum_orders: - orders += [SerumMarketOperations._serum_order_to_order(serum_order)] + orders += [Order.from_serum_order(serum_order)] return orders def __str__(self) -> str: - return f"""Β« πš‚πšŽπš›πšžπš–π™Ύπš›πšπšŽπš›π™Ώπš•πšŠπšŒπšŽπš› [{self.spot_market.symbol}] Β»""" + return f"""Β« πš‚πšŽπš›πšžπš–π™ΌπšŠπš›πš”πšŽπšπ™Ύπš™πšŽπš›πšŠπšπš’πš˜πš—πšœ [{self.serum_market.symbol}] Β»""" diff --git a/mango/spotmarket.py b/mango/spotmarket.py index 91fb3e7..5814640 100644 --- a/mango/spotmarket.py +++ b/mango/spotmarket.py @@ -27,9 +27,10 @@ from .token import Token class SpotMarket(Market): - def __init__(self, base: Token, quote: Token, address: PublicKey): + def __init__(self, base: Token, quote: Token, address: PublicKey, group_address: PublicKey): super().__init__(base, quote) self.address: PublicKey = address + self.group_address: PublicKey = group_address def __str__(self) -> str: return f"Β« πš‚πš™πš˜πšπ™ΌπšŠπš›πš”πšŽπš {self.symbol}: {self.address} Β»" diff --git a/mango/spotmarketoperations.py b/mango/spotmarketoperations.py new file mode 100644 index 0000000..6a915b7 --- /dev/null +++ b/mango/spotmarketoperations.py @@ -0,0 +1,168 @@ +# # ⚠ Warning +# +# 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. +# +# [πŸ₯­ 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) + + +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.account import Account as SolanaAccount +from solana.publickey import PublicKey +from solana.transaction import Transaction + +from .account import Account +from .accountinfo import AccountInfo +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 .wallet import Wallet + + +# # πŸ₯­ SpotMarketOperations class +# +# This class puts trades on the Serum orderbook. It doesn't do anything complicated. +# + +class SpotMarketOperations(MarketOperations): + def __init__(self, context: Context, wallet: Wallet, group: Group, account: Account, spot_market: SpotMarket, 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 + + 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 + + def report(text): + self.logger.info(text) + reporter(text) + + def just_log(text): + self.logger.info(text) + + if reporter is not None: + self.reporter = report + 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) -> 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] + + signers: typing.Sequence[SolanaAccount] = [self.wallet.account] + transaction = Transaction() + cancel_instructions = build_cancel_spot_order_instructions( + self.context, self.wallet, self.group, self.account, self.market, order, open_orders) + transaction.instructions.extend(cancel_instructions) + response = self.context.client.send_transaction(transaction, *signers, opts=self.context.transaction_options) + return self.context.unwrap_transaction_id_or_raise_exception(response) + + 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}.") + + open_orders = self.account.spot_open_orders[self.group_market_index] + 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}." + self.logger.info(report) + self.reporter(report) + + signers: typing.Sequence[SolanaAccount] = [self.wallet.account] + transaction = Transaction() + place_instructions = build_compound_spot_place_order_instructions( + self.context, self.wallet, self.group, self.account, self.market, payer_token_account.address, + open_orders, order_type, side, price, size, client_order_id, self.serum_fee_discount_token_address) + + transaction.instructions.extend(place_instructions) + response = self.context.client.send_transaction(transaction, *signers, opts=self.context.transaction_options) + self.context.unwrap_transaction_id_or_raise_exception(response) + + return Order(id=0, side=side, price=price, size=size, client_id=client_order_id, owner=self.account.address) + + def _load_serum_orders(self) -> typing.Sequence[SerumOrder]: + [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) + + return list(itertools.chain(bids_orderbook.orders(), asks_orderbook.orders())) + + def load_orders(self) -> typing.Sequence[Order]: + all_orders = self._load_serum_orders() + orders: typing.List[Order] = [] + for serum_order in all_orders: + orders += [Order.from_serum_order(serum_order)] + + 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: + return [] + + all_orders = self._load_serum_orders() + serum_orders = [o for o in all_orders if o.open_order_address == open_orders_account] + orders: typing.List[Order] = [] + for serum_order in serum_orders: + orders += [Order.from_serum_order(serum_order)] + + return orders + + def __str__(self) -> str: + return f"""Β« πš‚πš™πš˜πšπ™ΌπšŠπš›πš”πšŽπšπ™Ύπš™πšŽπš›πšŠπšπš’πš˜πš—πšœ [{self.spot_market.symbol}] Β»""" diff --git a/scripts/worlds-simplest-market-maker b/scripts/worlds-simplest-market-maker index 8631db1..6990e73 100755 --- a/scripts/worlds-simplest-market-maker +++ b/scripts/worlds-simplest-market-maker @@ -1,8 +1,8 @@ #!/usr/bin/env bash -MARKET="BTC-PERP" -POSITION_SIZE="0.01" -SLEEP_BETWEEN_ORDER_PLACES="60" +MARKET=${1:-BTC-PERP} +POSITION_SIZE=${2:-0.01} +SLEEP_BETWEEN_ORDER_PLACES=${3:-60} let fixed_spread=100 printf "Running on market %s with position size %f\nPress Control+C to stop...\n" $MARKET $POSITION_SIZE diff --git a/tests/fakes.py b/tests/fakes.py index 86f48db..b2ea50e 100644 --- a/tests/fakes.py +++ b/tests/fakes.py @@ -34,8 +34,8 @@ def fake_account_info(address: PublicKey = fake_public_key(), executable: bool = return mango.AccountInfo(address, executable, lamports, owner, rent_epoch, data) -def fake_token() -> mango.Token: - return mango.Token("FAKE", "Fake Token", fake_seeded_public_key("fake token"), Decimal(6)) +def fake_token(symbol: str = "FAKE") -> mango.Token: + return mango.Token(symbol, f"Fake Token ({symbol})", fake_seeded_public_key(f"fake token ({symbol})"), Decimal(6)) def fake_token_info() -> mango.TokenInfo: diff --git a/tests/test_liquidationevent.py b/tests/test_liquidationevent.py index f17e6cd..9087d85 100644 --- a/tests/test_liquidationevent.py +++ b/tests/test_liquidationevent.py @@ -1,5 +1,5 @@ from .context import mango -from .fakes import fake_context, fake_public_key +from .fakes import fake_public_key, fake_token from decimal import Decimal @@ -7,16 +7,15 @@ import datetime def test_liquidation_event(): - token_lookup = fake_context().token_lookup balances_before = [ - mango.TokenValue(token_lookup.find_by_symbol("ETH"), Decimal(1)), - mango.TokenValue(token_lookup.find_by_symbol("BTC"), Decimal("0.1")), - mango.TokenValue(token_lookup.find_by_symbol("USDT"), Decimal(1000)) + mango.TokenValue(fake_token("ETH"), Decimal(1)), + mango.TokenValue(fake_token("BTC"), Decimal("0.1")), + mango.TokenValue(fake_token("USDT"), Decimal(1000)) ] balances_after = [ - mango.TokenValue(token_lookup.find_by_symbol("ETH"), Decimal(1)), - mango.TokenValue(token_lookup.find_by_symbol("BTC"), Decimal("0.05")), - mango.TokenValue(token_lookup.find_by_symbol("USDT"), Decimal(2000)) + mango.TokenValue(fake_token("ETH"), Decimal(1)), + mango.TokenValue(fake_token("BTC"), Decimal("0.05")), + mango.TokenValue(fake_token("USDT"), Decimal(2000)) ] timestamp = datetime.datetime(2021, 5, 17, 12, 20, 56) event = mango.LiquidationEvent(timestamp, "Liquidator", "Group", True, "signature", diff --git a/tests/test_spotmarket.py b/tests/test_spotmarket.py index bb6d281..f9f94eb 100644 --- a/tests/test_spotmarket.py +++ b/tests/test_spotmarket.py @@ -1,16 +1,18 @@ from .context import mango +from .fakes import fake_seeded_public_key from decimal import Decimal -from solana.publickey import PublicKey def test_spot_market_constructor(): - address = PublicKey("11111111111111111111111111111114") - base = mango.Token("BASE", "Base Token", PublicKey("11111111111111111111111111111115"), Decimal(7)) - quote = mango.Token("QUOTE", "Quote Token", PublicKey("11111111111111111111111111111116"), Decimal(9)) - actual = mango.SpotMarket(base, quote, address) + address = fake_seeded_public_key("spot market address") + base = mango.Token("BASE", "Base Token", fake_seeded_public_key("base token"), Decimal(7)) + quote = mango.Token("QUOTE", "Quote Token", fake_seeded_public_key("quote token"), Decimal(9)) + group_address = fake_seeded_public_key("group address") + actual = mango.SpotMarket(base, quote, address, group_address) assert actual is not None assert actual.logger is not None - assert actual.address == address assert actual.base == base assert actual.quote == quote + assert actual.address == address + assert actual.group_address == group_address