From d22a09f208bb250dc3897a0bf2ae5c8faf2fe28d Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Fri, 23 Jul 2021 11:42:22 +0100 Subject: [PATCH] Added market 'stubs' to allow for delayed loading of markets. --- mango/__init__.py | 8 ++-- mango/account.py | 5 +++ mango/createmarketoperations.py | 35 ++++++++++-------- mango/idsjsonmarketlookup.py | 8 ++-- mango/market.py | 34 ++--------------- mango/oracles/serum/serum.py | 49 ++++++++++--------------- mango/perpmarket.py | 15 -------- mango/perpmarketinstructionbuilder.py | 2 - mango/perpmarketoperations.py | 2 - mango/perpsmarket.py | 53 ++++++++++++++++----------- mango/serummarket.py | 44 ++++++++++++---------- mango/serummarketlookup.py | 14 +++---- mango/serummarketoperations.py | 2 - mango/spotmarket.py | 48 +++++++++++++----------- mango/spotmarketoperations.py | 2 - tests/fakes.py | 4 +- tests/test_spotmarket.py | 4 +- 17 files changed, 148 insertions(+), 181 deletions(-) diff --git a/mango/__init__.py b/mango/__init__.py index 1397d69..df2a21c 100644 --- a/mango/__init__.py +++ b/mango/__init__.py @@ -20,7 +20,7 @@ from .instructiontype import InstructionType from .liquidatablereport import LiquidatableState, LiquidatableReport from .liquidationevent import LiquidationEvent from .liquidationprocessor import LiquidationProcessor, LiquidationProcessorState -from .market import AddressableMarket, InventorySource, Market +from .market import InventorySource, Market from .marketinstructionbuilder import MarketInstructionBuilder, NullMarketInstructionBuilder from .marketlookup import MarketLookup, NullMarketLookup, CompoundMarketLookup from .marketoperations import MarketOperations, NullMarketOperations @@ -38,16 +38,16 @@ from .perpmarket import PerpMarket from .perpmarketinfo import PerpMarketInfo from .perpmarketinstructionbuilder import PerpMarketInstructionBuilder from .perpmarketoperations import PerpMarketOperations -from .perpsmarket import PerpsMarket +from .perpsmarket import PerpsMarket, PerpsMarketStub from .reconnectingwebsocket import ReconnectingWebsocket from .retrier import RetryWithPauses, retry_context from .rootbank import NodeBank, RootBank -from .serummarket import SerumMarket +from .serummarket import SerumMarket, SerumMarketStub from .serummarketlookup import SerumMarketLookup from .serummarketinstructionbuilder import SerumMarketInstructionBuilder from .serummarketoperations import SerumMarketOperations from .spltokenlookup import SplTokenLookup -from .spotmarket import SpotMarket +from .spotmarket import SpotMarket, SpotMarketStub from .spotmarketinfo import SpotMarketInfo from .spotmarketinstructionbuilder import SpotMarketInstructionBuilder from .token import Token, SolToken diff --git a/mango/account.py b/mango/account.py index ab1acab..ff91eca 100644 --- a/mango/account.py +++ b/mango/account.py @@ -132,6 +132,11 @@ class Account(AddressableAccount): accounts += [account] return accounts + @staticmethod + def load_primary_for_owner(context: Context, owner: PublicKey, group: Group) -> "Account": + # Don't try to do anything smart (yet). Just return the first account. Might need to be smarter in the future. + return Account.load_all_for_owner(context, owner, group)[0] + def __str__(self): deposits = "\n ".join( [f"{deposit}" for deposit in self.deposits if deposit is not None and deposit.value != Decimal(0)] or ["None"]) diff --git a/mango/createmarketoperations.py b/mango/createmarketoperations.py index 5ed8f8f..37dcffc 100644 --- a/mango/createmarketoperations.py +++ b/mango/createmarketoperations.py @@ -21,11 +21,11 @@ from .market import Market from .marketoperations import MarketOperations, NullMarketOperations from .perpmarketinstructionbuilder import PerpMarketInstructionBuilder from .perpmarketoperations import PerpMarketOperations -from .perpsmarket import PerpsMarket -from .serummarket import SerumMarket +from .perpsmarket import PerpsMarket, PerpsMarketStub +from .serummarket import SerumMarket, SerumMarketStub from .serummarketinstructionbuilder import SerumMarketInstructionBuilder from .serummarketoperations import SerumMarketOperations -from .spotmarket import SpotMarket +from .spotmarket import SpotMarket, SpotMarketStub from .spotmarketinstructionbuilder import SpotMarketInstructionBuilder from .spotmarketoperations import SpotMarketOperations from .wallet import Wallet @@ -38,27 +38,30 @@ from .wallet import Wallet def create_market_operations(context: Context, wallet: Wallet, dry_run: bool, market: Market) -> MarketOperations: if dry_run: return NullMarketOperations(market.symbol) + elif isinstance(market, SerumMarketStub): + serum_market = market.load(context) + return create_market_operations(context, wallet, dry_run, serum_market) elif isinstance(market, SerumMarket): serum_market_instruction_builder: SerumMarketInstructionBuilder = SerumMarketInstructionBuilder.load( context, wallet, market) return SerumMarketOperations(context, wallet, market, serum_market_instruction_builder) + elif isinstance(market, SpotMarketStub): + group: Group = Group.load(context, market.group_address) + spot_market: SpotMarket = market.load(context, group) + return create_market_operations(context, wallet, dry_run, spot_market) elif isinstance(market, SpotMarket): - group = Group.load(context, market.group_address) - accounts = Account.load_all_for_owner(context, wallet.address, group) - account = accounts[0] + account: Account = Account.load_primary_for_owner(context, wallet.address, market.group) spot_market_instruction_builder: SpotMarketInstructionBuilder = SpotMarketInstructionBuilder.load( - context, wallet, group, account, market) - return SpotMarketOperations(context, wallet, group, account, market, spot_market_instruction_builder) - elif isinstance(market, PerpsMarket): + context, wallet, market.group, account, market) + return SpotMarketOperations(context, wallet, market.group, account, market, spot_market_instruction_builder) + elif isinstance(market, PerpsMarketStub): group = Group.load(context, market.group_address) - accounts = Account.load_all_for_owner(context, wallet.address, group) - account = accounts[0] - market.ensure_loaded(context) - if market.underlying_perp_market is None: - raise Exception(f"PerpsMarket {market.symbol} has not been loaded.") + perp_market: PerpsMarket = market.load(context, group) + return create_market_operations(context, wallet, dry_run, perp_market) + elif isinstance(market, PerpsMarket): + account = Account.load_primary_for_owner(context, wallet.address, market.group) perp_market_instruction_builder: PerpMarketInstructionBuilder = PerpMarketInstructionBuilder.load( context, wallet, market.underlying_perp_market.group, account, market) - return PerpMarketOperations(market.symbol, context, wallet, perp_market_instruction_builder, account, market) else: - raise Exception(f"Could not find order placer for market {market.symbol}") + raise Exception(f"Could not find market operations handler for market {market.symbol}") diff --git a/mango/idsjsonmarketlookup.py b/mango/idsjsonmarketlookup.py index 7cb5e24..5cf3f1d 100644 --- a/mango/idsjsonmarketlookup.py +++ b/mango/idsjsonmarketlookup.py @@ -22,8 +22,8 @@ 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 .perpsmarket import PerpsMarketStub +from .spotmarket import SpotMarketStub from .token import Token @@ -56,9 +56,9 @@ class IdsJsonMarketLookup(MarketLookup): quote = Token.find_by_symbol(tokens, quote_symbol) address = PublicKey(data["publicKey"]) if market_type == IdsJsonMarketType.PERP: - return PerpsMarket(base, quote, address, group_address) + return PerpsMarketStub(address, base, quote, group_address) else: - return SpotMarket(base, quote, address, group_address) + return SpotMarketStub(address, base, quote, group_address) @staticmethod def _load_tokens(data: typing.Dict) -> typing.Sequence[Token]: diff --git a/mango/market.py b/mango/market.py index 08e4859..725eb8e 100644 --- a/mango/market.py +++ b/mango/market.py @@ -17,7 +17,6 @@ import abc import enum import logging -import typing from solana.publickey import PublicKey @@ -37,12 +36,13 @@ class InventorySource(enum.Enum): # # πŸ₯­ Market class # -# This class describes a crypto market. It *must* have a base token and a quote token. +# This class describes a crypto market. It *must* have an address, a base token and a quote token. # class Market(metaclass=abc.ABCMeta): - def __init__(self, inventory_source: InventorySource, base: Token, quote: Token): + def __init__(self, address: PublicKey, inventory_source: InventorySource, base: Token, quote: Token): self.logger: logging.Logger = logging.getLogger(self.__class__.__name__) + self.address: PublicKey = address self.inventory_source: InventorySource = inventory_source self.base: Token = base self.quote: Token = quote @@ -51,36 +51,8 @@ class Market(metaclass=abc.ABCMeta): def symbol(self) -> str: return f"{self.base.symbol}/{self.quote.symbol}" - @abc.abstractmethod - def load(self, context: typing.Any) -> None: - raise NotImplementedError("Market.load() is not implemented on the base type.") - - @abc.abstractmethod - def ensure_loaded(self, context: typing.Any) -> None: - raise NotImplementedError("Market.ensure_loaded() is not implemented on the base type.") - def __str__(self) -> str: return f"Β« π™ΌπšŠπš›πš”πšŽπš {self.symbol} Β»" def __repr__(self) -> str: return f"{self}" - - -# # πŸ₯­ AddressableMarket class -# -# This class describes a crypto market. It *must* have a base token and a quote token. -# - -class AddressableMarket(Market): - def __init__(self, inventory_source: InventorySource, base: Token, quote: Token, address: PublicKey): - super().__init__(inventory_source, base, quote) - self.address: PublicKey = address - - def load(self, _: typing.Any) -> None: - pass - - def ensure_loaded(self, _: typing.Any) -> None: - pass - - def __str__(self) -> str: - return f"Β« π™°πšπšπš›πšŽπšœπšœπšŠπš‹πš•πšŽπ™ΌπšŠπš›πš”πšŽπš {self.symbol} [{self.address}] Β»" diff --git a/mango/oracles/serum/serum.py b/mango/oracles/serum/serum.py index 464ae85..613545d 100644 --- a/mango/oracles/serum/serum.py +++ b/mango/oracles/serum/serum.py @@ -21,17 +21,14 @@ import rx.operators as ops import typing from datetime import datetime -from decimal import Decimal -from pyserum.market.orderbook import OrderBook -from pyserum.market import Market as RawSerumMarket from solana.rpc.api import Client -from ...accountinfo import AccountInfo from ...context import Context -from ...market import AddressableMarket, Market +from ...market import Market from ...observables import observable_pipeline_error_reporter from ...oracle import Oracle, OracleProvider, OracleSource, Price -from ...serummarket import SerumMarket +from ...orders import Order, Side +from ...serummarket import SerumMarket, SerumMarketStub from ...serummarketlookup import SerumMarketLookup from ...spltokenlookup import SplTokenLookup from ...spotmarket import SpotMarket @@ -50,12 +47,11 @@ from ...spotmarket import SpotMarket class SerumOracle(Oracle): - def __init__(self, market: AddressableMarket): + def __init__(self, market: SerumMarket): name = f"Serum Oracle for {market.symbol}" super().__init__(name, market) - self.market: AddressableMarket = market + self.market: SerumMarket = market self.source: OracleSource = OracleSource("Serum", name, market) - self._serum_market: RawSerumMarket = None def fetch_price(self, context: Context) -> Price: # TODO: Do this right? @@ -64,27 +60,19 @@ class SerumOracle(Oracle): context.cluster_url = "https://solana-api.projectserum.com" context.client = Client(context.cluster_url) mainnet_serum_market_lookup: SerumMarketLookup = SerumMarketLookup.load(SplTokenLookup.DefaultDataFilepath) - mainnet_market = mainnet_serum_market_lookup.find_by_symbol(self.market.symbol) or self.market - adjusted_market: AddressableMarket = typing.cast(AddressableMarket, mainnet_market) - if self._serum_market is None: - self._serum_market = RawSerumMarket.load(context.client, adjusted_market.address, context.dex_program_id) + adjusted_market = self.market + mainnet_adjusted_market: typing.Optional[Market] = mainnet_serum_market_lookup.find_by_symbol( + self.market.symbol) + if mainnet_adjusted_market is not None: + adjusted_market_stub = typing.cast(SerumMarketStub, mainnet_adjusted_market) + adjusted_market = adjusted_market_stub.load(context) - bids_address = self._serum_market.state.bids() - asks_address = self._serum_market.state.asks() - bid_ask_account_infos = AccountInfo.load_multiple(context, [bids_address, asks_address]) - if len(bid_ask_account_infos) != 2: - raise Exception( - f"Failed to get bid/ask data from Serum for market address {adjusted_market.address} (bids: {bids_address}, asks: {asks_address}).") - bids = OrderBook.from_bytes(self._serum_market.state, bid_ask_account_infos[0].data) - asks = OrderBook.from_bytes(self._serum_market.state, bid_ask_account_infos[1].data) + orders: typing.Sequence[Order] = adjusted_market.orders(context) + top_bid = max([order.price for order in orders if order.side == Side.BUY]) + top_ask = min([order.price for order in orders if order.side == Side.SELL]) + mid_price = (top_bid + top_ask) / 2 - top_bid = list(bids.orders())[-1] - top_ask = list(asks.orders())[0] - top_bid_price = self.market.quote.round(Decimal(top_bid.info.price)) - top_ask_price = self.market.quote.round(Decimal(top_ask.info.price)) - mid_price = (top_bid_price + top_ask_price) / 2 - - return Price(self.source, datetime.now(), self.market, top_bid_price, mid_price, top_ask_price) + return Price(self.source, datetime.now(), self.market, top_bid, mid_price, top_ask) def to_streaming_observable(self, context: Context) -> rx.core.typing.Observable: return rx.interval(1).pipe( @@ -107,7 +95,8 @@ class SerumOracleProvider(OracleProvider): def oracle_for_market(self, context: Context, market: Market) -> typing.Optional[Oracle]: if isinstance(market, SpotMarket): - return SerumOracle(market) + serum_market = SerumMarket(market.address, market.base, market.quote, market.underlying_serum_market) + return SerumOracle(serum_market) elif isinstance(market, SerumMarket): return SerumOracle(market) else: @@ -116,7 +105,7 @@ class SerumOracleProvider(OracleProvider): if underlying_market is None: return None if isinstance(underlying_market, SpotMarket) or isinstance(underlying_market, SerumMarket): - return SerumOracle(underlying_market) + return self.oracle_for_market(context, underlying_market) return None diff --git a/mango/perpmarket.py b/mango/perpmarket.py index 63ed18b..69425ef 100644 --- a/mango/perpmarket.py +++ b/mango/perpmarket.py @@ -117,21 +117,6 @@ class PerpMarket(AddressableAccount): raise Exception(f"PerpMarket account not found at address '{address}'") return PerpMarket.parse(account_info, group) - @staticmethod - def load_with_group(context: Context, address: PublicKey) -> "PerpMarket": - account_info = AccountInfo.load(context, address) - if account_info is None: - raise Exception(f"PerpMarket account not found at address '{address}'") - - data = account_info.data - if len(data) != layouts.PERP_MARKET.sizeof(): - raise Exception( - f"PerpMarket data length ({len(data)}) does not match expected size ({layouts.PERP_MARKET.sizeof()})") - - layout = layouts.PERP_MARKET.parse(data) - group = Group.load(context, layout.group) - return PerpMarket.from_layout(layout, account_info, Version.V1, group) - def __str__(self): return f"""Β« π™ΏπšŽπš›πš™π™ΌπšŠπš›πš”πšŽπš {self.version} [{self.address}] {self.meta_data} diff --git a/mango/perpmarketinstructionbuilder.py b/mango/perpmarketinstructionbuilder.py index 197e0f6..2a847b3 100644 --- a/mango/perpmarketinstructionbuilder.py +++ b/mango/perpmarketinstructionbuilder.py @@ -47,8 +47,6 @@ class PerpMarketInstructionBuilder(MarketInstructionBuilder): self.account: Account = account self.perps_market: PerpsMarket = perps_market - self.perps_market.ensure_loaded(context) - @staticmethod def load(context: Context, wallet: Wallet, group: Group, account: Account, perps_market: PerpsMarket) -> "PerpMarketInstructionBuilder": return PerpMarketInstructionBuilder(context, wallet, group, account, perps_market) diff --git a/mango/perpmarketoperations.py b/mango/perpmarketoperations.py index faae4b7..28dc258 100644 --- a/mango/perpmarketoperations.py +++ b/mango/perpmarketoperations.py @@ -46,8 +46,6 @@ class PerpMarketOperations(MarketOperations): self.account: Account = account self.perps_market: PerpsMarket = perps_market - self.perps_market.ensure_loaded(context) - def cancel_order(self, order: Order) -> typing.Sequence[str]: self.logger.info(f"Cancelling {self.market_name} order {order}.") signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) diff --git a/mango/perpsmarket.py b/mango/perpsmarket.py index f9b65a2..e97ceed 100644 --- a/mango/perpsmarket.py +++ b/mango/perpsmarket.py @@ -19,7 +19,8 @@ from solana.publickey import PublicKey from .accountinfo import AccountInfo from .context import Context -from .market import AddressableMarket, InventorySource +from .group import Group +from .market import Market, InventorySource from .orderbookside import OrderBookSide from .orders import Order from .perpeventqueue import PerpEvent, PerpEventQueue @@ -32,29 +33,20 @@ from .token import Token # This class encapsulates our knowledge of a Mango perps market. # -class PerpsMarket(AddressableMarket): - def __init__(self, base: Token, quote: Token, address: PublicKey, group_address: PublicKey): - super().__init__(InventorySource.ACCOUNT, base, quote, address) - self.group_address: PublicKey = group_address - self.underlying_perp_market: typing.Optional[PerpMarket] = None - self.loaded: bool = False - - def load(self, context: Context) -> None: - self.underlying_perp_market = PerpMarket.load_with_group(context, self.address) - self.loaded = True - - def ensure_loaded(self, context: Context) -> None: - if not self.loaded: - self.load(context) +class PerpsMarket(Market): + def __init__(self, address: PublicKey, base: Token, quote: Token, underlying_perp_market: PerpMarket): + super().__init__(address, InventorySource.ACCOUNT, base, quote) + self.underlying_perp_market: PerpMarket = underlying_perp_market @property def symbol(self) -> str: return f"{self.base.symbol}-PERP" - def unprocessed_events(self, context: Context) -> typing.Sequence[PerpEvent]: - if self.underlying_perp_market is None: - raise Exception(f"PerpsMarket {self.symbol} has not been loaded.") + @property + def group(self) -> Group: + return self.underlying_perp_market.group + def unprocessed_events(self, context: Context) -> typing.Sequence[PerpEvent]: event_queue: PerpEventQueue = PerpEventQueue.load(context, self.underlying_perp_market.event_queue) return event_queue.unprocessed_events() @@ -77,9 +69,6 @@ class PerpsMarket(AddressableMarket): return distinct def orders(self, context: Context) -> typing.Sequence[Order]: - if self.underlying_perp_market is None: - raise Exception(f"PerpsMarket {self.symbol} has not been loaded.") - bids_address: PublicKey = self.underlying_perp_market.bids asks_address: PublicKey = self.underlying_perp_market.asks [bids, asks] = AccountInfo.load_multiple(context, [bids_address, asks_address]) @@ -89,3 +78,25 @@ class PerpsMarket(AddressableMarket): def __str__(self) -> str: return f"Β« π™ΏπšŽπš›πš™πšœπ™ΌπšŠπš›πš”πšŽπš {self.symbol} [{self.address}] Β»" + + +# # πŸ₯­ PerpsMarketStub class +# +# This class holds information to load a `PerpsMarket` object but doesn't automatically load it. +# + +class PerpsMarketStub(Market): + def __init__(self, address: PublicKey, base: Token, quote: Token, group_address: PublicKey): + super().__init__(address, InventorySource.ACCOUNT, base, quote) + self.group_address: PublicKey = group_address + + def load(self, context: Context, group: Group) -> PerpsMarket: + underlying_perp_market: PerpMarket = PerpMarket.load(context, self.address, group) + return PerpsMarket(self.address, self.base, self.quote, underlying_perp_market) + + @property + def symbol(self) -> str: + return f"{self.base.symbol}-PERP" + + def __str__(self) -> str: + return f"Β« π™ΏπšŽπš›πš™πšœπ™ΌπšŠπš›πš”πšŽπšπš‚πšπšžπš‹ {self.symbol} [{self.address}] Β»" diff --git a/mango/serummarket.py b/mango/serummarket.py index 1c2f01b..1c8cc8c 100644 --- a/mango/serummarket.py +++ b/mango/serummarket.py @@ -23,7 +23,7 @@ from solana.publickey import PublicKey from .accountinfo import AccountInfo from .context import Context -from .market import AddressableMarket, InventorySource +from .market import Market, InventorySource from .serumeventqueue import SerumEvent, SerumEventQueue from .token import Token @@ -34,31 +34,16 @@ from .token import Token # -class SerumMarket(AddressableMarket): - def __init__(self, base: Token, quote: Token, address: PublicKey): - super().__init__(InventorySource.SPL_TOKENS, base, quote, address) - self.underlying_serum_market: typing.Optional[PySerumMarket] = None - self.loaded: bool = False - - def load(self, context: Context) -> None: - self.underlying_serum_market = PySerumMarket.load(context.client, self.address, context.dex_program_id) - self.loaded = True - - def ensure_loaded(self, context: Context) -> None: - if not self.loaded: - self.load(context) +class SerumMarket(Market): + def __init__(self, address: PublicKey, base: Token, quote: Token, underlying_serum_market: PySerumMarket): + super().__init__(address, InventorySource.SPL_TOKENS, base, quote) + self.underlying_serum_market: PySerumMarket = underlying_serum_market def unprocessed_events(self, context: Context) -> typing.Sequence[SerumEvent]: - if self.underlying_serum_market is None: - raise Exception(f"SerumMarket {self.symbol} has not been loaded.") - event_queue: SerumEventQueue = SerumEventQueue.load(context, self.underlying_serum_market.state.event_queue()) return event_queue.unprocessed_events() def orders(self, context: Context) -> typing.Sequence[SerumOrder]: - if self.underlying_serum_market is None: - raise Exception(f"SerumMarket {self.symbol} has not been loaded.") - raw_market = self.underlying_serum_market [bids_info, asks_info] = AccountInfo.load_multiple( context, [raw_market.state.bids(), raw_market.state.asks()]) @@ -69,3 +54,22 @@ class SerumMarket(AddressableMarket): def __str__(self) -> str: return f"Β« πš‚πšŽπš›πšžπš–π™ΌπšŠπš›πš”πšŽπš {self.symbol} [{self.address}] Β»" + + +# # πŸ₯­ SerumMarketStub class +# +# This class holds information to load a `SerumMarket` object but doesn't automatically load it. +# + + +class SerumMarketStub(Market): + def __init__(self, address: PublicKey, base: Token, quote: Token): + super().__init__(address, InventorySource.SPL_TOKENS, base, quote) + + def load(self, context: Context) -> SerumMarket: + underlying_serum_market: PySerumMarket = PySerumMarket.load( + context.client, self.address, context.dex_program_id) + return SerumMarket(self.address, self.base, self.quote, underlying_serum_market) + + def __str__(self) -> str: + return f"Β« πš‚πšŽπš›πšžπš–π™ΌπšŠπš›πš”πšŽπšπš‚πšπšžπš‹ {self.symbol} [{self.address}] Β»" diff --git a/mango/serummarketlookup.py b/mango/serummarketlookup.py index 5b41174..1b3bb14 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 .serummarket import SerumMarket +from .serummarket import SerumMarketStub from .token import Token @@ -122,7 +122,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 SerumMarket(base, quote, market_address) + return SerumMarketStub(market_address, base, quote) def find_by_address(self, address: PublicKey) -> typing.Optional[Market]: address_string: str = str(address) @@ -139,7 +139,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 SerumMarket(base, quote, market_address) + return SerumMarketStub(market_address, base, quote) if "serumV3Usdt" in token_data["extensions"]: if token_data["extensions"]["serumV3Usdt"] == address_string: market_address_string = token_data["extensions"]["serumV3Usdt"] @@ -151,14 +151,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 SerumMarket(base, quote, market_address) + return SerumMarketStub(market_address, base, quote) 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[SerumMarket] = [] + all_markets: typing.List[SerumMarketStub] = [] for token_data in self.token_data["tokens"]: if "extensions" in token_data: if "serumV3Usdc" in token_data["extensions"]: @@ -166,12 +166,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 += [SerumMarket(base, usdc, market_address)] + all_markets += [SerumMarketStub(market_address, base, usdc)] 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 += [SerumMarket(base, usdt, market_address)] + all_markets += [SerumMarketStub(market_address, base, usdt)] return all_markets diff --git a/mango/serummarketoperations.py b/mango/serummarketoperations.py index 762ba75..a04bc70 100644 --- a/mango/serummarketoperations.py +++ b/mango/serummarketoperations.py @@ -42,8 +42,6 @@ class SerumMarketOperations(MarketOperations): self.serum_market: SerumMarket = serum_market self.market_instruction_builder: SerumMarketInstructionBuilder = market_instruction_builder - self.serum_market.ensure_loaded(context) - def cancel_order(self, order: Order) -> typing.Sequence[str]: self.logger.info(f"Cancelling {self.serum_market.symbol} order {order}.") signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) diff --git a/mango/spotmarket.py b/mango/spotmarket.py index c7aba7e..d3a646d 100644 --- a/mango/spotmarket.py +++ b/mango/spotmarket.py @@ -23,7 +23,8 @@ from solana.publickey import PublicKey from .accountinfo import AccountInfo from .context import Context -from .market import AddressableMarket, InventorySource +from .group import Group +from .market import Market, InventorySource from .serumeventqueue import SerumEvent, SerumEventQueue from .token import Token @@ -34,32 +35,17 @@ from .token import Token # -class SpotMarket(AddressableMarket): - def __init__(self, base: Token, quote: Token, address: PublicKey, group_address: PublicKey): - super().__init__(InventorySource.ACCOUNT, base, quote, address) - self.group_address: PublicKey = group_address - self.underlying_serum_market: typing.Optional[PySerumMarket] = None - self.loaded: bool = False - - def load(self, context: Context) -> None: - self.underlying_serum_market = PySerumMarket.load(context.client, self.address, context.dex_program_id) - self.loaded = True - - def ensure_loaded(self, context: Context) -> None: - if not self.loaded: - self.load(context) +class SpotMarket(Market): + def __init__(self, address: PublicKey, base: Token, quote: Token, group: Group, underlying_serum_market: PySerumMarket): + super().__init__(address, InventorySource.ACCOUNT, base, quote) + self.group: Group = group + self.underlying_serum_market: PySerumMarket = underlying_serum_market def unprocessed_events(self, context: Context) -> typing.Sequence[SerumEvent]: - if self.underlying_serum_market is None: - raise Exception(f"SpotMarket {self.symbol} has not been loaded.") - event_queue: SerumEventQueue = SerumEventQueue.load(context, self.underlying_serum_market.state.event_queue()) return event_queue.unprocessed_events() def orders(self, context: Context) -> typing.Sequence[SerumOrder]: - if self.underlying_serum_market is None: - raise Exception(f"SpotMarket {self.symbol} has not been loaded.") - raw_market = self.underlying_serum_market [bids_info, asks_info] = AccountInfo.load_multiple( context, [raw_market.state.bids(), raw_market.state.asks()]) @@ -70,3 +56,23 @@ class SpotMarket(AddressableMarket): def __str__(self) -> str: return f"Β« πš‚πš™πš˜πšπ™ΌπšŠπš›πš”πšŽπš {self.symbol} [{self.address}] Β»" + + +# # πŸ₯­ SpotMarketStub class +# +# This class holds information to load a `SpotMarket` object but doesn't automatically load it. +# + + +class SpotMarketStub(Market): + def __init__(self, address: PublicKey, base: Token, quote: Token, group_address: PublicKey): + super().__init__(address, InventorySource.ACCOUNT, base, quote) + self.group_address: PublicKey = group_address + + def load(self, context: Context, group: Group) -> SpotMarket: + underlying_serum_market: PySerumMarket = PySerumMarket.load( + context.client, self.address, context.dex_program_id) + return SpotMarket(self.address, self.base, self.quote, group, underlying_serum_market) + + def __str__(self) -> str: + return f"Β« πš‚πš™πš˜πšπ™ΌπšŠπš›πš”πšŽπšπš‚πšπšžπš‹ {self.symbol} [{self.address}] Β»" diff --git a/mango/spotmarketoperations.py b/mango/spotmarketoperations.py index d28359b..15d046f 100644 --- a/mango/spotmarketoperations.py +++ b/mango/spotmarketoperations.py @@ -48,8 +48,6 @@ class SpotMarketOperations(MarketOperations): self.market_index = group.find_spot_market_index(spot_market.address) self.open_orders_address = self.account.spot_open_orders[self.market_index] - self.spot_market.ensure_loaded(context) - def cancel_order(self, order: Order) -> typing.Sequence[str]: self.logger.info(f"Cancelling {self.spot_market.symbol} order {order}.") signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet) diff --git a/tests/fakes.py b/tests/fakes.py index 2197ade..8e7a59b 100644 --- a/tests/fakes.py +++ b/tests/fakes.py @@ -75,8 +75,8 @@ def fake_market() -> market.Market: return market.Market(None, state) -def fake_spot_market() -> mango.SpotMarket: - return mango.SpotMarket(fake_token("BASE"), fake_token("QUOTE"), fake_seeded_public_key("spot market"), fake_seeded_public_key("group address")) +def fake_spot_market_stub() -> mango.SpotMarketStub: + return mango.SpotMarketStub(fake_seeded_public_key("spot market"), fake_token("BASE"), fake_token("QUOTE"), fake_seeded_public_key("group address")) def fake_token_account() -> mango.TokenAccount: diff --git a/tests/test_spotmarket.py b/tests/test_spotmarket.py index f9f94eb..069bae8 100644 --- a/tests/test_spotmarket.py +++ b/tests/test_spotmarket.py @@ -4,12 +4,12 @@ from .fakes import fake_seeded_public_key from decimal import Decimal -def test_spot_market_constructor(): +def test_spot_market_stub_constructor(): 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) + actual = mango.SpotMarketStub(address, base, quote, group_address) assert actual is not None assert actual.logger is not None assert actual.base == base