Added market 'stubs' to allow for delayed loading of markets.

This commit is contained in:
Geoff Taylor 2021-07-23 11:42:22 +01:00
parent 8fe48de527
commit d22a09f208
17 changed files with 148 additions and 181 deletions

View File

@ -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

View File

@ -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"])

View File

@ -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}")

View File

@ -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]:

View File

@ -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}] »"

View File

@ -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

View File

@ -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}

View File

@ -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)

View File

@ -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)

View File

@ -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}] »"

View File

@ -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}] »"

View File

@ -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

View File

@ -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)

View File

@ -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}] »"

View File

@ -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)

View File

@ -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:

View File

@ -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