# # ⚠ Warning # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT # LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN # NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # # [πŸ₯­ Mango Markets](https://mango.markets/) support is available at: # [Docs](https://docs.mango.markets/) # [Discord](https://discord.gg/67jySBhxrg) # [Twitter](https://twitter.com/mangomarkets) # [Github](https://github.com/blockworks-foundation) # [Email](mailto:hello@blockworks.foundation) import abc import pyserum.enums import typing from datetime import datetime from decimal import Decimal from solana.publickey import PublicKey from .accountinfo import AccountInfo from .addressableaccount import AddressableAccount from .context import Context from .layouts import layouts from .lotsizeconverter import LotSizeConverter from .metadata import Metadata from .orders import Side from .version import Version # # πŸ₯­ PerpEvent class # # `PerpEvent` is the base class of all perp event objects. # class PerpEvent(metaclass=abc.ABCMeta): def __init__(self, event_type: int): self.event_type: int = event_type @abc.abstractproperty @property def accounts_to_crank(self) -> typing.Sequence[PublicKey]: raise NotImplementedError("PerpEvent.accounts_to_crank is not implemented on the base type.") def __repr__(self) -> str: return f"{self}" # # πŸ₯­ PerpFillEvent class # # `PerpOutEvent` stores details of a perp 'fill' event. # class PerpFillEvent(PerpEvent): def __init__(self, event_type: int, timestamp: datetime, side: Side, price: Decimal, quantity: Decimal, best_initial: Decimal, maker_slot: Decimal, maker_out: bool, maker: PublicKey, maker_order_id: Decimal, maker_client_order_id: Decimal, taker: PublicKey, taker_order_id: Decimal, taker_client_order_id: Decimal): super().__init__(event_type) self.timestamp: datetime = timestamp self.side: Side = side self.price: Decimal = price self.quantity: Decimal = quantity self.best_initial: Decimal = best_initial self.maker_slot: Decimal = maker_slot self.maker_out: bool = maker_out self.maker: PublicKey = maker self.maker_order_id: Decimal = maker_order_id self.maker_client_order_id: Decimal = maker_client_order_id self.taker: PublicKey = taker self.taker_order_id: Decimal = taker_order_id self.taker_client_order_id: Decimal = taker_client_order_id @property def accounts_to_crank(self) -> typing.Sequence[PublicKey]: return [self.maker, self.taker] def __str__(self): return f"""Β« π™ΏπšŽπš›πš™π™΅πš’πš•πš•π™΄πšŸπšŽπš—πš [{self.timestamp}] {self.side} {self.quantity:,.8f} at {self.price:,.8f} Maker: {self.maker}, {self.maker_order_id} / {self.maker_client_order_id} Taker: {self.taker}, {self.taker_order_id} / {self.taker_client_order_id} Best Initial: {self.best_initial} Maker Slot: {self.maker_slot} Maker Out: {self.maker_out} Β»""" # # πŸ₯­ PerpOutEvent class # # `PerpOutEvent` stores details of a perp 'out' event. # class PerpOutEvent(PerpEvent): def __init__(self, event_type: int, owner: PublicKey, side: Side, quantity: Decimal, slot: Decimal): super().__init__(event_type) self.owner: PublicKey = owner self.side: Side = side self.slot: Decimal = slot self.quantity: Decimal = quantity @property def accounts_to_crank(self) -> typing.Sequence[PublicKey]: return [self.owner] def __str__(self): return f"""Β« π™ΏπšŽπš›πš™π™Ύπšžπšπ™΄πšŸπšŽπš—πš [{self.owner}] {self.side} {self.quantity}, slot: {self.slot} Β»""" # # πŸ₯­ PerpUnknownEvent class # # `PerpUnknownEvent` details an unknown `PerpEvent`. This should never be encountered, but might if # the event queue data is upgraded before this code. # class PerpUnknownEvent(PerpEvent): def __init__(self, event_type: int, owner: PublicKey): super().__init__(event_type) self.owner: PublicKey = owner @property def accounts_to_crank(self) -> typing.Sequence[PublicKey]: return [self.owner] def __str__(self): return f"Β« π™ΏπšŽπš›πš™πš„πš—πš”πš—πš˜πš πš—π™΄πšŸπšŽπš—πš [{self.owner}] Β»" # # πŸ₯­ event_builder function # # `event_builder()` takes an event layout and returns a typed `PerpEvent`. # def event_builder(lot_size_converter: LotSizeConverter, event_layout) -> typing.Optional[PerpEvent]: if event_layout.event_type == b'\x00': if event_layout.maker is None and event_layout.taker is None: return None side: Side = Side.BUY if event_layout.side == pyserum.enums.Side.BUY else Side.SELL quantity: Decimal = lot_size_converter.quantity_lots_to_value(event_layout.quantity) price: Decimal = lot_size_converter.price_lots_to_value(event_layout.price) return PerpFillEvent(event_layout.event_type, event_layout.timestamp, side, price, quantity, event_layout.best_initial, event_layout.maker_slot, event_layout.maker_out, event_layout.maker, event_layout.maker_order_id, event_layout.maker_client_order_id, event_layout.taker, event_layout.taker_order_id, event_layout.taker_client_order_id) elif event_layout.event_type == b'\x01': return PerpOutEvent(event_layout.event_type, event_layout.owner, event_layout.side, event_layout.quantity, event_layout.slot) else: return PerpUnknownEvent(event_layout.event_type, event_layout.owner) # # πŸ₯­ PerpEventQueue class # # `PerpEventQueue` stores details of perp events in a ringbuffer, along with indices to track which events are # processed by 'consume events' and which are not. # class PerpEventQueue(AddressableAccount): def __init__(self, account_info: AccountInfo, version: Version, meta_data: Metadata, head: Decimal, count: Decimal, sequence_number: Decimal, events: typing.Sequence[typing.Optional[PerpEvent]]): super().__init__(account_info) self.version: Version = version self.meta_data: Metadata = meta_data self.head: Decimal = head self.count: Decimal = count self.sequence_number: Decimal = sequence_number self.events: typing.Sequence[typing.Optional[PerpEvent]] = events @staticmethod def from_layout(layout: layouts.PERP_EVENT_QUEUE, account_info: AccountInfo, version: Version, lot_size_converter: LotSizeConverter) -> "PerpEventQueue": meta_data: Metadata = Metadata.from_layout(layout.meta_data) head: Decimal = layout.head count: Decimal = layout.count seq_num: Decimal = layout.seq_num events: typing.List[typing.Optional[PerpEvent]] = [] for raw_event in layout.events: built_event = event_builder(lot_size_converter, raw_event) events += [built_event] return PerpEventQueue(account_info, version, meta_data, head, count, seq_num, events) @ staticmethod def parse(account_info: AccountInfo, lot_size_converter: LotSizeConverter) -> "PerpEventQueue": # Data length isn't fixed so can't check we get the right value the way we normally do. layout = layouts.PERP_EVENT_QUEUE.parse(account_info.data) return PerpEventQueue.from_layout(layout, account_info, Version.V1, lot_size_converter) @ staticmethod def load(context: Context, address: PublicKey, lot_size_converter: LotSizeConverter) -> "PerpEventQueue": account_info = AccountInfo.load(context, address) if account_info is None: raise Exception(f"PerpEventQueue account not found at address '{address}'") return PerpEventQueue.parse(account_info, lot_size_converter) @property def capacity(self) -> int: return len(self.events) def unprocessed_events(self) -> typing.Sequence[PerpEvent]: unprocessed: typing.List[PerpEvent] = [] for index in range(int(self.count)): modulo_index = (self.head + index) % self.capacity event: typing.Optional[PerpEvent] = self.events[int(modulo_index)] if event is None: raise Exception(f"Event at index {index} should not be None.") unprocessed += [event] return unprocessed def __str__(self): events = "\n ".join([f"{event}".replace("\n", "\n ") for event in self.events if event is not None]) or "None" return f"""Β« π™ΏπšŽπš›πš™π™΄πšŸπšŽπš—πšπš€πšžπšŽπšžπšŽ [{self.version}] {self.address} {self.meta_data} Head: {self.head} Count: {self.count} Sequence Number: {self.sequence_number} Capacity: {self.capacity} Events: {events} Β»""" # # πŸ₯­ UnseenPerpEventChangesTracker class # # `UnseenPerpEventChangesTracker` tracks changes to a specific `PerpEventQueue`. When an updated version of the # `PerpEventQueue` is passed to `unseen()`, any new events are returned. # # Seen events are tracked by keeping a 'head' index of the last event seen in the `PerpEventQueue` ringbuffer. # When a new `PerpEventQueue` appears, its head+count is used to calculate the new seen head, and events from # the old seen head to the new seen head are returned. # class UnseenPerpEventChangesTracker: def __init__(self, initial: PerpEventQueue): self.last_head: Decimal = initial.head def unseen(self, event_queue: PerpEventQueue) -> typing.Sequence[typing.Optional[PerpEvent]]: unseen: typing.List[PerpEvent] = [] new_head: Decimal = event_queue.head if self.last_head != new_head: to_process: int = int((event_queue.capacity + new_head - self.last_head) % event_queue.capacity) for index in range(to_process): modulo_index = (self.last_head + index) % event_queue.capacity event: typing.Optional[PerpEvent] = event_queue.events[int(modulo_index)] if event is None: raise Exception(f"Event at index {index} should not be None.") unseen += [event] self.last_head = new_head % event_queue.capacity return unseen