Improved perp cranking and added public key sorting tests.

This commit is contained in:
Geoff Taylor 2022-01-11 12:24:15 +00:00
parent 59394060e9
commit 6352844ed1
17 changed files with 248 additions and 57 deletions

View File

@ -122,6 +122,8 @@ from .marketoperations import MarketOperations as MarketOperations
from .marketoperations import NullMarketInstructionBuilder as NullMarketInstructionBuilder
from .marketoperations import NullMarketOperations as NullMarketOperations
from .metadata import Metadata as Metadata
from .modelstate import EventQueue as EventQueue
from .modelstate import NullEventQueue as NullEventQueue
from .modelstate import ModelState as ModelState
from .notification import CompoundNotificationTarget as CompoundNotificationTarget
from .notification import ConsoleNotificationTarget as ConsoleNotificationTarget
@ -239,6 +241,9 @@ from .watchers import build_perp_open_orders_watcher as build_perp_open_orders_w
from .watchers import build_price_watcher as build_price_watcher
from .watchers import build_serum_inventory_watcher as build_serum_inventory_watcher
from .watchers import build_orderbook_watcher as build_orderbook_watcher
from .watchers import build_serum_event_queue_watcher as build_serum_event_queue_watcher
from .watchers import build_spot_event_queue_watcher as build_spot_event_queue_watcher
from .watchers import build_perp_event_queue_watcher as build_perp_event_queue_watcher
from .websocketsubscription import IndividualWebSocketSubscriptionManager as IndividualWebSocketSubscriptionManager
from .websocketsubscription import SharedWebSocketSubscriptionManager as SharedWebSocketSubscriptionManager
from .websocketsubscription import WebSocketAccountSubscription as WebSocketAccountSubscription

View File

@ -97,7 +97,7 @@ Ignore:
place_order = self.market_instruction_builder.build_place_order_instructions(to_place_with_client_id)
place_orders += place_order
crank = self.market_instruction_builder.build_crank_instructions([])
crank = self.market_instruction_builder.build_crank_instructions(model_state.accounts_to_crank)
settle = self.market_instruction_builder.build_settle_instructions()
redeem = mango.CombinableInstructions.empty()

View File

@ -88,7 +88,7 @@ class PollingModelStateBuilder(ModelStateBuilder):
def from_values(self, order_owner: PublicKey, market: mango.Market, group: mango.Group, account: mango.Account,
price: mango.Price, placed_orders_container: mango.PlacedOrdersContainer,
inventory: mango.Inventory, orderbook: mango.OrderBook) -> ModelState:
inventory: mango.Inventory, orderbook: mango.OrderBook, event_queue: mango.EventQueue) -> ModelState:
group_watcher: mango.ManualUpdateWatcher[mango.Group] = mango.ManualUpdateWatcher(group)
account_watcher: mango.ManualUpdateWatcher[mango.Account] = mango.ManualUpdateWatcher(account)
price_watcher: mango.ManualUpdateWatcher[mango.Price] = mango.ManualUpdateWatcher(price)
@ -96,9 +96,11 @@ class PollingModelStateBuilder(ModelStateBuilder):
mango.PlacedOrdersContainer] = mango.ManualUpdateWatcher(placed_orders_container)
inventory_watcher: mango.ManualUpdateWatcher[mango.Inventory] = mango.ManualUpdateWatcher(inventory)
orderbook_watcher: mango.ManualUpdateWatcher[mango.OrderBook] = mango.ManualUpdateWatcher(orderbook)
event_queue_watcher: mango.ManualUpdateWatcher[mango.EventQueue] = mango.ManualUpdateWatcher(event_queue)
return ModelState(order_owner, market, group_watcher, account_watcher, price_watcher,
placed_orders_container_watcher, inventory_watcher, orderbook_watcher)
placed_orders_container_watcher, inventory_watcher, orderbook_watcher,
event_queue_watcher)
def __str__(self) -> str:
return "« PollingModelStateBuilder »"
@ -145,7 +147,8 @@ class SerumPollingModelStateBuilder(PollingModelStateBuilder):
self.base_inventory_token_account.address,
self.quote_inventory_token_account.address,
self.market.bids_address,
self.market.asks_address
self.market.asks_address,
self.market.event_queue_address
]
account_infos: typing.Sequence[mango.AccountInfo] = mango.AccountInfo.load_multiple(context, addresses)
group: mango.Group = mango.Group.parse_with_context(context, account_infos[0])
@ -162,6 +165,8 @@ class SerumPollingModelStateBuilder(PollingModelStateBuilder):
orderbook: mango.OrderBook = self.market.parse_account_infos_to_orderbook(account_infos[6], account_infos[7])
event_queue: mango.EventQueue = mango.SerumEventQueue.parse(account_infos[8])
price: mango.Price = self.oracle.fetch_price(context)
available: Decimal = (base_inventory_token_account.value.value * price.mid_price) + \
@ -173,7 +178,7 @@ class SerumPollingModelStateBuilder(PollingModelStateBuilder):
base_inventory_token_account.value,
quote_inventory_token_account.value)
return self.from_values(self.order_owner, self.market, group, account, price, placed_orders_container, inventory, orderbook)
return self.from_values(self.order_owner, self.market, group, account, price, placed_orders_container, inventory, orderbook, event_queue)
def __str__(self) -> str:
return f"""« SerumPollingModelStateBuilder for market '{self.market.symbol}' »"""
@ -214,6 +219,7 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder):
self.account_address,
self.market.bids_address,
self.market.asks_address,
self.market.event_queue_address,
*self.all_open_orders_addresses
]
account_infos: typing.Sequence[mango.AccountInfo] = mango.AccountInfo.load_multiple(context, addresses)
@ -225,7 +231,7 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder):
self.all_open_orders_addresses = account.spot_open_orders
spot_open_orders_account_infos_by_address = {
str(account_info.address): account_info for account_info in account_infos[5:]}
str(account_info.address): account_info for account_info in account_infos[6:]}
all_open_orders: typing.Dict[str, mango.OpenOrders] = {}
for basket_token in account.slots:
@ -256,9 +262,11 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder):
orderbook: mango.OrderBook = self.market.parse_account_infos_to_orderbook(account_infos[3], account_infos[4])
event_queue: mango.EventQueue = mango.SerumEventQueue.parse(account_infos[5])
price: mango.Price = self.oracle.fetch_price(context)
return self.from_values(self.order_owner, self.market, group, account, price, placed_orders_container, inventory, orderbook)
return self.from_values(self.order_owner, self.market, group, account, price, placed_orders_container, inventory, orderbook, event_queue)
def __str__(self) -> str:
return f"""« SpotPollingModelStateBuilder for market '{self.market.symbol}' »"""
@ -294,7 +302,8 @@ class PerpPollingModelStateBuilder(PollingModelStateBuilder):
self.cache_address,
self.account_address,
self.market.underlying_perp_market.bids,
self.market.underlying_perp_market.asks
self.market.underlying_perp_market.asks,
self.market.event_queue_address
]
account_infos: typing.Sequence[mango.AccountInfo] = mango.AccountInfo.load_multiple(context, addresses)
group: mango.Group = mango.Group.parse_with_context(context, account_infos[0])
@ -320,9 +329,11 @@ class PerpPollingModelStateBuilder(PollingModelStateBuilder):
orderbook: mango.OrderBook = self.market.parse_account_infos_to_orderbook(account_infos[3], account_infos[4])
event_queue: mango.EventQueue = mango.PerpEventQueue.parse(account_infos[5], self.market.lot_size_converter)
price: mango.Price = self.oracle.fetch_price(context)
return self.from_values(self.order_owner, self.market, group, account, price, placed_orders_container, inventory, orderbook)
return self.from_values(self.order_owner, self.market, group, account, price, placed_orders_container, inventory, orderbook, event_queue)
def __str__(self) -> str:
return f"""« PerpPollingModelStateBuilder for market '{self.market.symbol}' »"""

View File

@ -130,7 +130,9 @@ def _websocket_model_state_builder_factory(context: mango.Context, disposer: man
context, websocket_manager, health_check, disposer, wallet, market, price_watcher)
latest_open_orders_observer: mango.Watcher[mango.PlacedOrdersContainer] = mango.build_serum_open_orders_watcher(
context, websocket_manager, health_check, market, wallet)
latest_orderbook_watcher = mango.build_orderbook_watcher(
latest_orderbook_watcher: mango.Watcher[mango.OrderBook] = mango.build_orderbook_watcher(
context, websocket_manager, health_check, market)
latest_event_queue_watcher: mango.Watcher[mango.EventQueue] = mango.build_serum_event_queue_watcher(
context, websocket_manager, health_check, market)
elif isinstance(market, mango.SpotMarket):
market_index: int = group.slot_by_spot_market_address(market.address).index
@ -155,6 +157,8 @@ def _websocket_model_state_builder_factory(context: mango.Context, disposer: man
market, latest_account_observer, group_watcher, all_open_orders_watchers, cache_watcher)
latest_orderbook_watcher = mango.build_orderbook_watcher(
context, websocket_manager, health_check, market)
latest_event_queue_watcher = mango.build_spot_event_queue_watcher(
context, websocket_manager, health_check, market)
elif isinstance(market, mango.PerpMarket):
order_owner = account.address
inventory_watcher = mango.PerpInventoryAccountWatcher(
@ -163,10 +167,12 @@ def _websocket_model_state_builder_factory(context: mango.Context, disposer: man
context, websocket_manager, health_check, market, account, group, account_subscription)
latest_orderbook_watcher = mango.build_orderbook_watcher(
context, websocket_manager, health_check, market)
latest_event_queue_watcher = mango.build_perp_event_queue_watcher(
context, websocket_manager, health_check, market)
else:
raise Exception(f"Could not determine type of market {market.symbol}")
model_state = ModelState(order_owner, market, group_watcher, latest_account_observer,
latest_price_observer, latest_open_orders_observer,
inventory_watcher, latest_orderbook_watcher)
inventory_watcher, latest_orderbook_watcher, latest_event_queue_watcher)
return WebsocketModelStateBuilder(model_state)

View File

@ -77,7 +77,7 @@ class MarketInstructionBuilder(metaclass=abc.ABCMeta):
"MarketInstructionBuilder.build_settle_instructions() is not implemented on the base type.")
@abc.abstractmethod
def build_crank_instructions(self, open_orders_addresses: typing.Sequence[PublicKey], limit: Decimal = Decimal(32)) -> CombinableInstructions:
def build_crank_instructions(self, addresses: typing.Sequence[PublicKey], limit: Decimal = Decimal(32)) -> CombinableInstructions:
raise NotImplementedError(
"MarketInstructionBuilder.build_crank_instructions() is not implemented on the base type.")
@ -164,7 +164,7 @@ class NullMarketInstructionBuilder(MarketInstructionBuilder):
def build_settle_instructions(self) -> CombinableInstructions:
return CombinableInstructions.empty()
def build_crank_instructions(self, addresses_to_crank: typing.Sequence[PublicKey], limit: Decimal = Decimal(32)) -> CombinableInstructions:
def build_crank_instructions(self, addresses: typing.Sequence[PublicKey], limit: Decimal = Decimal(32)) -> CombinableInstructions:
return CombinableInstructions.empty()
def build_redeem_instructions(self) -> CombinableInstructions:

View File

@ -30,6 +30,25 @@ from .placedorder import PlacedOrdersContainer
from .watcher import Watcher
# # 🥭 EventQueue protocol
#
# The `EventQueue` protocol just says the object has a property `accounts_to_crank` that returns a `Sequence` of `PublicKey`s.
#
# This is to share the interface between a `SerumEventQueue` and a `PerpEventQueue` for cranking, when the
# underlying objects are really quite different.
#
class EventQueue(typing.Protocol):
@property
def accounts_to_crank(self) -> typing.Sequence[PublicKey]:
raise NotImplementedError("EventQueue.accounts_to_crank is not implemented on the Protocol.")
class NullEventQueue:
@property
def accounts_to_crank(self) -> typing.Sequence[PublicKey]:
return []
# # 🥭 ModelState class
#
# Provides simple access to the latest state of market and account data.
@ -43,7 +62,8 @@ class ModelState:
price_watcher: Watcher[Price],
placed_orders_container_watcher: Watcher[PlacedOrdersContainer],
inventory_watcher: Watcher[Inventory],
orderbook: Watcher[OrderBook]
orderbook: Watcher[OrderBook],
event_queue: Watcher[EventQueue]
) -> None:
self._logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.order_owner: PublicKey = order_owner
@ -55,6 +75,7 @@ class ModelState:
PlacedOrdersContainer] = placed_orders_container_watcher
self.inventory_watcher: Watcher[Inventory] = inventory_watcher
self.orderbook_watcher: Watcher[OrderBook] = orderbook
self.event_queue_watcher: Watcher[EventQueue] = event_queue
self.not_quoting: bool = False
self.state: typing.Dict[str, typing.Any] = {}
@ -105,6 +126,10 @@ class ModelState:
def spread(self) -> Decimal:
return self.orderbook.spread
@property
def accounts_to_crank(self) -> typing.Sequence[PublicKey]:
return self.event_queue_watcher.latest.accounts_to_crank
def current_orders(self) -> typing.Sequence[Order]:
self.orderbook
all_orders = [*self.bids, *self.asks]

View File

@ -237,6 +237,22 @@ class PerpEventQueue(AddressableAccount):
raise Exception(f"PerpEventQueue account not found at address '{address}'")
return PerpEventQueue.parse(account_info, lot_size_converter)
@property
def accounts_to_crank(self) -> typing.Sequence[PublicKey]:
to_crank: typing.List[PublicKey] = []
for event_to_crank in self.unprocessed_events:
to_crank += event_to_crank.accounts_to_crank
seen = []
distinct = []
for account in to_crank:
account_str = account.to_base58()
if account_str not in seen:
distinct += [account]
seen += [account_str]
return distinct
@ property
def capacity(self) -> int:
return len(self.unprocessed_events) + len(self.processed_events)

View File

@ -112,6 +112,10 @@ class PerpMarket(LoadedMarket):
def asks_address(self) -> PublicKey:
return self.underlying_perp_market.asks
@property
def event_queue_address(self) -> PublicKey:
return self.underlying_perp_market.event_queue
def parse_account_info_to_orders(self, account_info: AccountInfo) -> typing.Sequence[Order]:
side: PerpOrderBookSide = PerpOrderBookSide.parse(account_info, self.underlying_perp_market)
return side.orders()
@ -124,8 +128,7 @@ class PerpMarket(LoadedMarket):
return FundingRate.from_stats_data(self.symbol, self.lot_size_converter, oldest_stats, newest_stats)
def unprocessed_events(self, context: Context) -> typing.Sequence[PerpEvent]:
event_queue: PerpEventQueue = PerpEventQueue.load(
context, self.underlying_perp_market.event_queue, self.lot_size_converter)
event_queue: PerpEventQueue = PerpEventQueue.load(context, self.event_queue_address, self.lot_size_converter)
return event_queue.unprocessed_events
def accounts_to_crank(self, context: Context, additional_account_to_crank: typing.Optional[PublicKey]) -> typing.Sequence[PublicKey]:

View File

@ -71,22 +71,20 @@ class PerpMarketInstructionBuilder(MarketInstructionBuilder):
def build_settle_instructions(self) -> CombinableInstructions:
return CombinableInstructions.empty()
def build_crank_instructions(self, account_addresses: typing.Sequence[PublicKey], limit: Decimal = Decimal(32)) -> CombinableInstructions:
def build_crank_instructions(self, addresses: typing.Sequence[PublicKey], limit: Decimal = Decimal(32)) -> CombinableInstructions:
if self.perp_market.underlying_perp_market is None:
raise Exception(f"PerpMarket {self.perp_market.symbol} has not been loaded.")
addresses_to_crank: typing.Sequence[PublicKey] = [*account_addresses, self.account.address]
distinct_addresses_to_crank: typing.List[PublicKey] = []
for address in addresses_to_crank:
if address not in distinct_addresses_to_crank:
distinct_addresses_to_crank += [address]
distinct_addresses: typing.List[PublicKey] = [self.account.address]
for address in addresses:
if address not in distinct_addresses:
distinct_addresses += [address]
limited_addresses_to_crank = distinct_addresses_to_crank[0:min(
int(limit), len(distinct_addresses_to_crank))]
limited_addresses = distinct_addresses[0:min(int(limit), len(distinct_addresses))]
limited_addresses.sort(key=encode_public_key_for_sorting)
self._logger.debug(f"About to crank {len(limited_addresses)} addresses: {limited_addresses}")
limited_addresses_to_crank.sort(key=encode_public_key_for_sorting)
return build_mango_consume_events_instructions(self.context, self.group, self.perp_market.underlying_perp_market, limited_addresses_to_crank, limit)
return build_mango_consume_events_instructions(self.context, self.group, self.perp_market.underlying_perp_market, limited_addresses, limit)
def build_redeem_instructions(self) -> CombinableInstructions:
return build_redeem_accrued_mango_instructions(self.context, self.wallet, self.perp_market, self.group, self.account, self.mngo_token_bank)
@ -141,7 +139,7 @@ class PerpMarketOperations(MarketOperations):
def crank(self, limit: Decimal = Decimal(32)) -> typing.Sequence[str]:
signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet)
accounts_to_crank = self.perp_market.accounts_to_crank(self.context, None)
accounts_to_crank = self.perp_market.accounts_to_crank(self.context, self.account.address)
crank = self.market_instruction_builder.build_crank_instructions(accounts_to_crank, limit)
return (signers + crank).execute(self.context)

View File

@ -160,6 +160,22 @@ class SerumEventQueue(AddressableAccount):
raise Exception(f"SerumEventQueue account not found at address '{address}'")
return SerumEventQueue.parse(account_info)
@property
def accounts_to_crank(self) -> typing.Sequence[PublicKey]:
to_crank: typing.List[PublicKey] = []
for event_to_crank in self.unprocessed_events:
to_crank += [event_to_crank.public_key]
seen = []
distinct = []
for account in to_crank:
account_str = account.to_base58()
if account_str not in seen:
distinct += [account]
seen += [account_str]
return distinct
@property
def capacity(self) -> int:
return len(self.unprocessed_events) + len(self.processed_events)

View File

@ -54,12 +54,16 @@ class SerumMarket(LoadedMarket):
def asks_address(self) -> PublicKey:
return self.underlying_serum_market.state.asks()
@property
def event_queue_address(self) -> PublicKey:
return self.underlying_serum_market.state.event_queue()
def parse_account_info_to_orders(self, account_info: AccountInfo) -> typing.Sequence[Order]:
orderbook: PySerumOrderBook = PySerumOrderBook.from_bytes(self.underlying_serum_market.state, account_info.data)
return list(map(Order.from_serum_order, orderbook.orders()))
def unprocessed_events(self, context: Context) -> typing.Sequence[SerumEvent]:
event_queue: SerumEventQueue = SerumEventQueue.load(context, self.underlying_serum_market.state.event_queue())
event_queue: SerumEventQueue = SerumEventQueue.load(context, self.event_queue_address)
return event_queue.unprocessed_events
def find_openorders_address_for_owner(self, context: Context, owner: PublicKey) -> typing.Optional[PublicKey]:

View File

@ -120,23 +120,20 @@ class SerumMarketInstructionBuilder(MarketInstructionBuilder):
return CombinableInstructions.empty()
return build_serum_settle_instructions(self.context, self.wallet, self.raw_market, self.open_orders_address, self.base_token_account.address, self.quote_token_account.address)
def build_crank_instructions(self, open_orders_addresses: typing.Sequence[PublicKey], limit: Decimal = Decimal(32)) -> CombinableInstructions:
def build_crank_instructions(self, addresses: typing.Sequence[PublicKey], limit: Decimal = Decimal(32)) -> CombinableInstructions:
if self.open_orders_address is None:
self._logger.debug("Returning empty crank instructions - no serum OpenOrders address provided.")
return CombinableInstructions.empty()
open_orders_to_crank: typing.Sequence[PublicKey] = [*open_orders_addresses, self.open_orders_address]
distinct_open_orders_addresses: typing.List[PublicKey] = []
for oo in open_orders_to_crank:
if oo not in distinct_open_orders_addresses:
distinct_open_orders_addresses += [oo]
distinct_addresses: typing.List[PublicKey] = [self.open_orders_address]
for oo in addresses:
if oo not in distinct_addresses:
distinct_addresses += [oo]
limited_open_orders_addresses = distinct_open_orders_addresses[0:min(
int(limit), len(distinct_open_orders_addresses))]
limited_addresses = distinct_addresses[0:min(int(limit), len(distinct_addresses))]
limited_addresses.sort(key=encode_public_key_for_sorting)
limited_open_orders_addresses.sort(key=encode_public_key_for_sorting)
return build_serum_consume_events_instructions(self.context, self.serum_market.address, self.raw_market.state.event_queue(), limited_open_orders_addresses, int(limit))
return build_serum_consume_events_instructions(self.context, self.serum_market.address, self.raw_market.state.event_queue(), limited_addresses, int(limit))
def build_create_openorders_instructions(self) -> CombinableInstructions:
create_open_orders = build_create_serum_open_orders_instructions(self.context, self.wallet, self.raw_market)

View File

@ -55,12 +55,16 @@ class SpotMarket(LoadedMarket):
def asks_address(self) -> PublicKey:
return self.underlying_serum_market.state.asks()
@property
def event_queue_address(self) -> PublicKey:
return self.underlying_serum_market.state.event_queue()
def parse_account_info_to_orders(self, account_info: AccountInfo) -> typing.Sequence[Order]:
orderbook: PySerumOrderBook = PySerumOrderBook.from_bytes(self.underlying_serum_market.state, account_info.data)
return list(map(Order.from_serum_order, orderbook.orders()))
def unprocessed_events(self, context: Context) -> typing.Sequence[SerumEvent]:
event_queue: SerumEventQueue = SerumEventQueue.load(context, self.underlying_serum_market.state.event_queue())
event_queue: SerumEventQueue = SerumEventQueue.load(context, self.event_queue_address)
return event_queue.unprocessed_events
def __str__(self) -> str:

View File

@ -108,23 +108,20 @@ class SpotMarketInstructionBuilder(MarketInstructionBuilder):
self.raw_market, self.group, self.open_orders_address,
base_rootbank, base_nodebank, quote_rootbank, quote_nodebank)
def build_crank_instructions(self, open_orders_addresses: typing.Sequence[PublicKey], limit: Decimal = Decimal(32)) -> CombinableInstructions:
def build_crank_instructions(self, addresses: typing.Sequence[PublicKey], limit: Decimal = Decimal(32)) -> CombinableInstructions:
if self.open_orders_address is None:
self._logger.debug("Returning empty crank instructions - no spot OpenOrders address provided.")
return CombinableInstructions.empty()
open_orders_to_crank: typing.Sequence[PublicKey] = [*open_orders_addresses, self.open_orders_address]
distinct_open_orders_addresses: typing.List[PublicKey] = []
for oo in open_orders_to_crank:
if oo not in distinct_open_orders_addresses:
distinct_open_orders_addresses += [oo]
distinct_addresses: typing.List[PublicKey] = [self.open_orders_address]
for oo in addresses:
if oo not in distinct_addresses:
distinct_addresses += [oo]
limited_open_orders_addresses = distinct_open_orders_addresses[0:min(
int(limit), len(distinct_open_orders_addresses))]
limited_addresses = distinct_addresses[0:min(int(limit), len(distinct_addresses))]
limited_addresses.sort(key=encode_public_key_for_sorting)
limited_open_orders_addresses.sort(key=encode_public_key_for_sorting)
return build_serum_consume_events_instructions(self.context, self.spot_market.address, self.raw_market.state.event_queue(), limited_open_orders_addresses, int(limit))
return build_serum_consume_events_instructions(self.context, self.spot_market.address, self.raw_market.state.event_queue(), limited_addresses, int(limit))
def build_redeem_instructions(self) -> CombinableInstructions:
return CombinableInstructions.empty()

View File

@ -32,14 +32,17 @@ from .instrumentvalue import InstrumentValue
from .inventory import Inventory
from .loadedmarket import LoadedMarket
from .market import Market, InventorySource
from .modelstate import EventQueue
from .observables import DisposePropagator, LatestItemObserverSubscriber
from .openorders import OpenOrders
from .oracle import Price
from .oracle import OracleProvider
from .oraclefactory import create_oracle_provider
from .orders import OrderBook
from .perpeventqueue import PerpEventQueue
from .perpmarket import PerpMarket
from .placedorder import PlacedOrdersContainer
from .serumeventqueue import SerumEventQueue
from .serummarket import SerumMarket
from .spotmarket import SpotMarket
from .spotmarketoperations import SpotMarketInstructionBuilder, SpotMarketOperations
@ -240,3 +243,36 @@ def build_orderbook_watcher(context: Context, manager: WebSocketSubscriptionMana
health_check.add("orderbook_bids_subscription", bids_subscription.publisher)
health_check.add("orderbook_asks_subscription", asks_subscription.publisher)
return orderbook_observer
def build_serum_event_queue_watcher(context: Context, manager: WebSocketSubscriptionManager, health_check: HealthCheck, serum_market: SerumMarket) -> Watcher[EventQueue]:
initial: EventQueue = SerumEventQueue.load(context, serum_market.event_queue_address)
subscription = WebSocketAccountSubscription[EventQueue](
context, serum_market.event_queue_address, lambda account_info: SerumEventQueue.parse(account_info))
manager.add(subscription)
latest_observer = LatestItemObserverSubscriber[EventQueue](initial)
subscription.publisher.subscribe(latest_observer)
health_check.add("event_queue_subscription", subscription.publisher)
return latest_observer
def build_spot_event_queue_watcher(context: Context, manager: WebSocketSubscriptionManager, health_check: HealthCheck, spot_market: SpotMarket) -> Watcher[EventQueue]:
initial: EventQueue = SerumEventQueue.load(context, spot_market.event_queue_address)
subscription = WebSocketAccountSubscription[EventQueue](
context, spot_market.event_queue_address, lambda account_info: SerumEventQueue.parse(account_info))
manager.add(subscription)
latest_observer = LatestItemObserverSubscriber[EventQueue](initial)
subscription.publisher.subscribe(latest_observer)
health_check.add("event_queue_subscription", subscription.publisher)
return latest_observer
def build_perp_event_queue_watcher(context: Context, manager: WebSocketSubscriptionManager, health_check: HealthCheck, perp_market: PerpMarket) -> Watcher[EventQueue]:
initial: EventQueue = PerpEventQueue.load(context, perp_market.event_queue_address, perp_market.lot_size_converter)
subscription = WebSocketAccountSubscription[EventQueue](
context, perp_market.event_queue_address, lambda account_info: PerpEventQueue.parse(account_info, perp_market.lot_size_converter))
manager.add(subscription)
latest_observer = LatestItemObserverSubscriber[EventQueue](initial)
subscription.publisher.subscribe(latest_observer)
health_check.add("event_queue_subscription", subscription.publisher)
return latest_observer

View File

@ -255,7 +255,8 @@ def fake_model_state(order_owner: typing.Optional[PublicKey] = None,
price: typing.Optional[mango.Price] = None,
placed_orders_container: typing.Optional[mango.PlacedOrdersContainer] = None,
inventory: typing.Optional[mango.Inventory] = None,
orderbook: typing.Optional[mango.OrderBook] = None) -> mango.ModelState:
orderbook: typing.Optional[mango.OrderBook] = None,
event_queue: typing.Optional[mango.EventQueue] = None) -> mango.ModelState:
order_owner = order_owner or fake_seeded_public_key("order owner")
market = market or fake_loaded_market()
group = group or fake_group()
@ -264,6 +265,7 @@ def fake_model_state(order_owner: typing.Optional[PublicKey] = None,
placed_orders_container = placed_orders_container or fake_placed_orders_container()
inventory = inventory or fake_inventory()
orderbook = orderbook or mango.OrderBook("FAKE", NullLotSizeConverter(), fake_bids(), fake_asks())
event_queue = event_queue or mango.NullEventQueue()
group_watcher: mango.ManualUpdateWatcher[mango.Group] = mango.ManualUpdateWatcher(group)
account_watcher: mango.ManualUpdateWatcher[mango.Account] = mango.ManualUpdateWatcher(account)
price_watcher: mango.ManualUpdateWatcher[mango.Price] = mango.ManualUpdateWatcher(price)
@ -271,10 +273,11 @@ def fake_model_state(order_owner: typing.Optional[PublicKey] = None,
mango.PlacedOrdersContainer] = mango.ManualUpdateWatcher(placed_orders_container)
inventory_watcher: mango.ManualUpdateWatcher[mango.Inventory] = mango.ManualUpdateWatcher(inventory)
orderbook_watcher: mango.ManualUpdateWatcher[mango.OrderBook] = mango.ManualUpdateWatcher(orderbook)
event_queue_watcher: mango.ManualUpdateWatcher[mango.EventQueue] = mango.ManualUpdateWatcher(event_queue)
return mango.ModelState(order_owner, market, group_watcher,
account_watcher, price_watcher, placed_orders_container_watcher,
inventory_watcher, orderbook_watcher)
return mango.ModelState(order_owner, market, group_watcher, account_watcher, price_watcher,
placed_orders_container_watcher, inventory_watcher, orderbook_watcher,
event_queue_watcher)
def fake_mango_instruction() -> mango.MangoInstruction:

View File

@ -234,5 +234,75 @@ def test_publickey_sorting() -> None:
test_keys.sort(key=mango.encode_public_key_for_sorting)
for counter in range(100):
for counter in range(len(test_keys)):
assert test_keys[counter] == expected[counter], f"Index {counter} - {test_keys[counter]} does not match expected {expected[counter]}"
# This is the same test but with results from sorting in a BPF Solana runtime.
#
# This runtime is very limited so 14 is the maximum number of keys that I've been able to sort there.
#
def test_publickey_bpf_sorting() -> None:
test_keys = [
PublicKey("FaQzFknCYS1Dt1T8d7wXr6a8dxSZwgb1YyiR1pmWJBC"),
PublicKey("AuAYgwDerZryPif7Zw1ZqACYgJFRqmKwy3ZqASr2Wu7d"),
PublicKey("79bShRscEARLjd1gzYz7LMC76ft87AQZxcRk3ZPg7thf"),
PublicKey("HMbNVVb6uqqhuTaQU4RmKDeG3VZ1pvRn3PgnhfevbjWJ"),
PublicKey("FFzYWt9K2ZDeyxpDbvKbma6232baDHoCFGL1gZAKiox1"),
PublicKey("CpFj2d5uYjeh34FKh6iYRTE2dL3N9NaSrZtyZVZ7eQwa"),
PublicKey("FyjuBBN5fUHjtpB5LbSVW1mMocWCBebWJEecr1YN1TaQ"),
PublicKey("2zBRHRBd8mieLZ4nvYpJqw8D9Bc9tRhgnrBvLxw5rswv"),
PublicKey("GFQFVSEYN9ho4UwA4KJefTGrrnV8xKwyT2U5VoY4ABRw"),
PublicKey("E9UL1uyqoU6KNvQhn4dRhJ85jzu9zSQ8mnKK6zG6K2LH"),
PublicKey("789UPSUbj9TYSm12e66qo8PCE7RvDygqFKyy7qvCjtBD"),
PublicKey("Hgbt3PYF3CPjJxwgurNPtU7PxKWZawxbVYAY3PHuqcRY"),
PublicKey("jD2gcCANgvQM54brip9jT9L3PHfG3YmoBALQeWQ2QFt"),
PublicKey("8ZuYuQdGesgcKs6UaiHZNo44iijnsFJ4zUEF3KymUhjw")
]
expected = [
PublicKey("FaQzFknCYS1Dt1T8d7wXr6a8dxSZwgb1YyiR1pmWJBC"),
PublicKey("jD2gcCANgvQM54brip9jT9L3PHfG3YmoBALQeWQ2QFt"),
PublicKey("2zBRHRBd8mieLZ4nvYpJqw8D9Bc9tRhgnrBvLxw5rswv"),
PublicKey("789UPSUbj9TYSm12e66qo8PCE7RvDygqFKyy7qvCjtBD"),
PublicKey("79bShRscEARLjd1gzYz7LMC76ft87AQZxcRk3ZPg7thf"),
PublicKey("8ZuYuQdGesgcKs6UaiHZNo44iijnsFJ4zUEF3KymUhjw"),
PublicKey("AuAYgwDerZryPif7Zw1ZqACYgJFRqmKwy3ZqASr2Wu7d"),
PublicKey("CpFj2d5uYjeh34FKh6iYRTE2dL3N9NaSrZtyZVZ7eQwa"),
PublicKey("E9UL1uyqoU6KNvQhn4dRhJ85jzu9zSQ8mnKK6zG6K2LH"),
PublicKey("FFzYWt9K2ZDeyxpDbvKbma6232baDHoCFGL1gZAKiox1"),
PublicKey("FyjuBBN5fUHjtpB5LbSVW1mMocWCBebWJEecr1YN1TaQ"),
PublicKey("GFQFVSEYN9ho4UwA4KJefTGrrnV8xKwyT2U5VoY4ABRw"),
PublicKey("HMbNVVb6uqqhuTaQU4RmKDeG3VZ1pvRn3PgnhfevbjWJ"),
PublicKey("Hgbt3PYF3CPjJxwgurNPtU7PxKWZawxbVYAY3PHuqcRY")
]
test_keys.sort(key=mango.encode_public_key_for_sorting)
for counter in range(len(test_keys)):
assert test_keys[counter] == expected[counter], f"Index {counter} - {test_keys[counter]} does not match expected {expected[counter]}"
# This is a short test to help with debugging, with results from the BPF Solana runtime.
#
def test_publickey_short_sorting() -> None:
test_keys = [
PublicKey("AuAYgwDerZryPif7Zw1ZqACYgJFRqmKwy3ZqASr2Wu7d"),
PublicKey("79bShRscEARLjd1gzYz7LMC76ft87AQZxcRk3ZPg7thf"),
PublicKey("HMbNVVb6uqqhuTaQU4RmKDeG3VZ1pvRn3PgnhfevbjWJ"),
PublicKey("FFzYWt9K2ZDeyxpDbvKbma6232baDHoCFGL1gZAKiox1"),
PublicKey("CpFj2d5uYjeh34FKh6iYRTE2dL3N9NaSrZtyZVZ7eQwa"),
]
expected = [
PublicKey("79bShRscEARLjd1gzYz7LMC76ft87AQZxcRk3ZPg7thf"),
PublicKey("AuAYgwDerZryPif7Zw1ZqACYgJFRqmKwy3ZqASr2Wu7d"),
PublicKey("CpFj2d5uYjeh34FKh6iYRTE2dL3N9NaSrZtyZVZ7eQwa"),
PublicKey("FFzYWt9K2ZDeyxpDbvKbma6232baDHoCFGL1gZAKiox1"),
PublicKey("HMbNVVb6uqqhuTaQU4RmKDeG3VZ1pvRn3PgnhfevbjWJ")
]
test_keys.sort(key=mango.encode_public_key_for_sorting)
for counter in range(len(test_keys)):
assert test_keys[counter] == expected[counter], f"Index {counter} - {test_keys[counter]} does not match expected {expected[counter]}"