Added market 'stubs' to allow for delayed loading of markets.
This commit is contained in:
parent
8fe48de527
commit
d22a09f208
|
@ -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
|
||||
|
|
|
@ -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"])
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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]:
|
||||
|
|
|
@ -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}] »"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}] »"
|
||||
|
|
|
@ -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}] »"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}] »"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue