diff --git a/bin/marketmaker b/bin/marketmaker index 9b387fa..5743960 100755 --- a/bin/marketmaker +++ b/bin/marketmaker @@ -82,7 +82,7 @@ def build_latest_account_observer(context: mango.Context, account: mango.Account return account_subscription, latest_account_observer -def build_latest_spot_open_orders_observer(manager: mango.WebSocketSubscriptionManager, disposer: mango.DisposePropagator, spot_market: mango.SpotMarket) -> mango.LatestItemObserverSubscriber[mango.OpenOrders]: +def build_latest_spot_open_orders_observer(manager: mango.WebSocketSubscriptionManager, disposer: mango.DisposePropagator, spot_market: mango.SpotMarket) -> mango.LatestItemObserverSubscriber[mango.PlacedOrdersContainer]: market_index = group.find_spot_market_index(spot_market.address) spot_open_orders_address = account.spot_open_orders[market_index] if spot_open_orders_address is None: @@ -93,13 +93,14 @@ def build_latest_spot_open_orders_observer(manager: mango.WebSocketSubscriptionM manager.add(spot_open_orders_subscription) initial_spot_open_orders = mango.OpenOrders.load( context, spot_open_orders_address, spot_market.base.decimals, spot_market.quote.decimals) - latest_open_orders_observer = mango.LatestItemObserverSubscriber(initial_spot_open_orders) + latest_open_orders_observer = mango.LatestItemObserverSubscriber[mango.PlacedOrdersContainer]( + initial_spot_open_orders) spot_open_orders_subscription.publisher.subscribe(latest_open_orders_observer) add_file_health("open_orders_subscription", spot_open_orders_subscription.publisher, disposer) return latest_open_orders_observer -def build_latest_serum_open_orders_observer(manager: mango.WebSocketSubscriptionManager, disposer: mango.DisposePropagator, serum_market: mango.SerumMarket, context: mango.Context, wallet: mango.Wallet) -> mango.LatestItemObserverSubscriber[mango.OpenOrders]: +def build_latest_serum_open_orders_observer(manager: mango.WebSocketSubscriptionManager, disposer: mango.DisposePropagator, serum_market: mango.SerumMarket, context: mango.Context, wallet: mango.Wallet) -> mango.LatestItemObserverSubscriber[mango.PlacedOrdersContainer]: all_open_orders = mango.OpenOrders.load_for_market_and_owner( context, serum_market.address, wallet.address, context.dex_program_id, serum_market.base.decimals, serum_market.quote.decimals) if len(all_open_orders) > 0: @@ -124,24 +125,19 @@ def build_latest_serum_open_orders_observer(manager: mango.WebSocketSubscription manager.add(serum_open_orders_subscription) - latest_serum_open_orders_observer = mango.LatestItemObserverSubscriber(initial_serum_open_orders) + latest_serum_open_orders_observer = mango.LatestItemObserverSubscriber[mango.PlacedOrdersContainer]( + initial_serum_open_orders) serum_open_orders_subscription.publisher.subscribe(latest_serum_open_orders_observer) add_file_health("open_orders_subscription", serum_open_orders_subscription.publisher, disposer) return latest_serum_open_orders_observer -def build_latest_perp_open_orders_observer(disposer: mango.DisposePropagator, perp_market: mango.PerpMarket, account: mango.Account, account_subscription: mango.WebSocketSubscription[mango.Account]) -> mango.LatestItemObserverSubscriber[mango.OpenOrders]: +def build_latest_perp_open_orders_observer(disposer: mango.DisposePropagator, perp_market: mango.PerpMarket, account: mango.Account, account_subscription: mango.WebSocketSubscription[mango.Account]) -> mango.LatestItemObserverSubscriber[mango.PlacedOrdersContainer]: index = group.find_perp_market_index(perp_market.address) - - def _build_open_orders_from_account(account: mango.Account) -> mango.OpenOrders: - perp_account = account.perp_accounts[index] - perp_open_orders = perp_account.open_orders - return mango.OpenOrders.from_perps_account_layout(context, account, perp_market, perp_open_orders) - - initial_open_orders = _build_open_orders_from_account(account) - latest_open_orders_observer = mango.LatestItemObserverSubscriber(initial_open_orders) + initial_open_orders = account.perp_accounts[index].open_orders + latest_open_orders_observer = mango.LatestItemObserverSubscriber[mango.PlacedOrdersContainer](initial_open_orders) account_subscription.publisher.subscribe( - on_next=lambda account: latest_open_orders_observer.on_next(_build_open_orders_from_account(account))) + on_next=lambda updated_account: latest_open_orders_observer.on_next(updated_account.perp_accounts[index].open_orders)) add_file_health("open_orders_subscription", account_subscription.publisher, disposer) return latest_open_orders_observer diff --git a/mango/__init__.py b/mango/__init__.py index e294fea..80daea1 100644 --- a/mango/__init__.py +++ b/mango/__init__.py @@ -30,18 +30,21 @@ from .marketoperations import MarketOperations, NullMarketOperations from .metadata import Metadata from .notification import NotificationTarget, TelegramNotificationTarget, DiscordNotificationTarget, MailjetNotificationTarget, CsvFileNotificationTarget, FilteringNotificationTarget, NotificationHandler, parse_subscription_target from .observables import DisposePropagator, NullObserverSubscriber, PrintingObserverSubscriber, TimestampedPrintingObserverSubscriber, CollectingObserverSubscriber, LatestItemObserverSubscriber, CaptureFirstItem, FunctionObserver, create_backpressure_skipping_observer, debug_print_item, log_subscription_error, observable_pipeline_error_reporter, EventSource, FileToucherObserver -from .openorders import OpenOrders, PlacedOrder +from .openorders import OpenOrders from .oracle import OracleSource, Price, Oracle, OracleProvider, SupportedOracleFeature from .orderbookside import OrderBookSide from .orders import Order, OrderType, Side from .ownedtokenvalue import OwnedTokenValue from .oraclefactory import create_oracle_provider +from .perpaccount import PerpAccount from .perpeventqueue import PerpEvent, PerpFillEvent, PerpOutEvent, PerpUnknownEvent, PerpEventQueue, UnseenPerpEventChangesTracker from .perpmarket import PerpMarket, PerpMarketStub from .perpmarketdetails import PerpMarketDetails from .perpmarketinfo import PerpMarketInfo from .perpmarketinstructionbuilder import PerpMarketInstructionBuilder from .perpmarketoperations import PerpMarketOperations +from .perpopenorders import PerpOpenOrders +from .placedorder import PlacedOrder, PlacedOrdersContainer from .reconnectingwebsocket import ReconnectingWebsocket from .retrier import RetryWithPauses, retry_context from .rootbank import NodeBank, RootBank diff --git a/mango/account.py b/mango/account.py index f82114e..532ada9 100644 --- a/mango/account.py +++ b/mango/account.py @@ -26,6 +26,7 @@ from .encoding import encode_key from .group import Group from .layouts import layouts from .metadata import Metadata +from .perpaccount import PerpAccount from .tokenvalue import TokenValue from .version import Version @@ -40,7 +41,7 @@ class Account(AddressableAccount): meta_data: Metadata, group: Group, owner: PublicKey, in_margin_basket: typing.Sequence[Decimal], deposits: typing.Sequence[typing.Optional[TokenValue]], borrows: typing.Sequence[typing.Optional[TokenValue]], net_assets: typing.Sequence[typing.Optional[TokenValue]], spot_open_orders: typing.Sequence[PublicKey], - perp_accounts: typing.Sequence[typing.Any], msrm_amount: Decimal, being_liquidated: bool, + perp_accounts: typing.Sequence[PerpAccount], msrm_amount: Decimal, being_liquidated: bool, is_bankrupt: bool): super().__init__(account_info) self.version: Version = version @@ -53,7 +54,7 @@ class Account(AddressableAccount): self.borrows: typing.Sequence[typing.Optional[TokenValue]] = borrows self.net_assets: typing.Sequence[typing.Optional[TokenValue]] = net_assets self.spot_open_orders: typing.Sequence[PublicKey] = spot_open_orders - self.perp_accounts: typing.Sequence[layouts.PERP_ACCOUNT] = perp_accounts + self.perp_accounts: typing.Sequence[PerpAccount] = perp_accounts self.msrm_amount: Decimal = msrm_amount self.being_liquidated: bool = being_liquidated self.is_bankrupt: bool = is_bankrupt @@ -81,10 +82,10 @@ class Account(AddressableAccount): net_assets += [None] spot_open_orders: typing.Sequence[PublicKey] = layout.spot_open_orders - perp_accounts: typing.Sequence[typing.Any] = layout.perp_accounts + perp_accounts: typing.Sequence[PerpAccount] = list(map(PerpAccount.from_layout, layout.perp_accounts)) msrm_amount: Decimal = layout.msrm_amount - being_liquidated: bool = layout.being_liquidated - is_bankrupt: bool = layout.is_bankrupt + being_liquidated: bool = bool(layout.being_liquidated) + is_bankrupt: bool = bool(layout.is_bankrupt) return Account(account_info, version, meta_data, group, owner, in_margin_basket, deposits, borrows, net_assets, spot_open_orders, perp_accounts, msrm_amount, being_liquidated, is_bankrupt) @@ -96,7 +97,7 @@ class Account(AddressableAccount): f"Account data length ({len(data)}) does not match expected size ({layouts.MANGO_ACCOUNT.sizeof()})") layout = layouts.MANGO_ACCOUNT.parse(data) - return Account.from_layout(layout, account_info, Version.V1, group) + return Account.from_layout(layout, account_info, Version.V3, group) @staticmethod def load(context: Context, address: PublicKey, group: Group) -> "Account": @@ -138,24 +139,34 @@ class Account(AddressableAccount): 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"]) - borrows = "\n ".join( - [f"{borrow}" for borrow in self.borrows if borrow is not None and borrow.value != Decimal(0)] or ["None"]) - net_assets = "\n ".join( - [f"{net_asset}" for net_asset in self.net_assets if net_asset is not None and net_asset.value != Decimal(0)] or ["None"]) + def _render_list(items, stub): + rendered = [] + for index, item in enumerate(items): + rendered += [f"{index}: {(item or stub)}".replace("\n", "\n ")] + return rendered + available_deposit_count = len([deposit for deposit in self.deposits if deposit is not None]) + deposits = "\n ".join(_render_list(self.deposits, "« No Deposit »")) + available_borrow_count = len([borrow for borrow in self.borrows if borrow is not None]) + borrows = "\n ".join(_render_list(self.borrows, "« No Borrow »")) + net_assets = "\n ".join(_render_list(self.net_assets, "« No Net Assets »")) spot_open_orders = ", ".join([f"{oo}" for oo in self.spot_open_orders if oo is not None]) perp_accounts = ", ".join( [f"{perp}".replace("\n", "\n ") for perp in self.perp_accounts if perp.open_orders.free_slot_bits != 0xFFFFFFFF]) + indices_in_basket = [] + for index, value in enumerate(self.in_margin_basket): + if value != 0: + indices_in_basket += [index] + in_margin_basket = ", ".join([f"{self.group.tokens[index].token.symbol}" for index in indices_in_basket]) return f"""« 𝙰𝚌𝚌𝚘𝚞𝚗𝚝 {self.version} [{self.address}] {self.meta_data} - Bankrupt? {self.is_bankrupt} - Being Liquidated? {self.being_liquidated} Owner: {self.owner} Group: « 𝙶𝚛𝚘𝚞𝚙 '{self.group.name}' {self.group.version} [{self.group.address}] » - Deposits: + In Basket: {in_margin_basket} + Bankrupt? {self.is_bankrupt} + Being Liquidated? {self.being_liquidated} + Deposits [{available_deposit_count} available]: {deposits} - Borrows: + Borrows [{available_borrow_count} available]: {borrows} Net Assets: {net_assets} diff --git a/mango/group.py b/mango/group.py index 310248b..6ea89f0 100644 --- a/mango/group.py +++ b/mango/group.py @@ -113,7 +113,7 @@ class Group(AddressableAccount): layout = layouts.GROUP.parse(data) name = context.lookup_group_name(account_info.address) - return Group.from_layout(context, layout, name, account_info, Version.V1, context.token_lookup, context.market_lookup) + return Group.from_layout(context, layout, name, account_info, Version.V3, context.token_lookup, context.market_lookup) @staticmethod def load(context: Context, address: typing.Optional[PublicKey] = None) -> "Group": @@ -156,9 +156,6 @@ class Group(AddressableAccount): return balances def __str__(self): - print("Token 14", self.tokens[14]) - print("Token 14 is None", self.tokens[14] is None) - def _render_list(items, stub): rendered = [] for index, item in enumerate(items): diff --git a/mango/instructions.py b/mango/instructions.py index 154b4d7..ea18e5e 100644 --- a/mango/instructions.py +++ b/mango/instructions.py @@ -646,10 +646,6 @@ def build_spot_place_order_instructions(context: Context, wallet: Wallet, group: relevant_open_orders += [AccountMeta(is_signer=False, is_writable=False, pubkey=oo_address or SYSTEM_PROGRAM_ADDRESS)] - print("root_bank.address", root_bank.address) - print("node_bank.address", node_bank.address) - print("open_orders_address", open_orders_address) - print("account.address", account.address) fee_discount_address_meta: typing.List[AccountMeta] = [] if fee_discount_address is not None: fee_discount_address_meta = [AccountMeta(is_signer=False, is_writable=False, pubkey=fee_discount_address)] diff --git a/mango/marketmaking/modelstate.py b/mango/marketmaking/modelstate.py index 90abf79..122f4e6 100644 --- a/mango/marketmaking/modelstate.py +++ b/mango/marketmaking/modelstate.py @@ -30,7 +30,7 @@ class ModelState: group_watcher: mango.LatestItemObserverSubscriber[mango.Group], price_watcher: mango.LatestItemObserverSubscriber[mango.Price], perp_market_watcher: typing.Optional[mango.LatestItemObserverSubscriber[mango.PerpMarketDetails]], - open_orders_watcher: mango.LatestItemObserverSubscriber[mango.OpenOrders] + placed_orders_container_watcher: mango.LatestItemObserverSubscriber[mango.PlacedOrdersContainer] ): self.logger: logging.Logger = logging.getLogger(self.__class__.__name__) self.market: mango.Market = market @@ -39,7 +39,8 @@ class ModelState: self.price_watcher: mango.LatestItemObserverSubscriber[mango.Price] = price_watcher self.perp_market_watcher: typing.Optional[mango.LatestItemObserverSubscriber[mango.PerpMarketDetails] ] = perp_market_watcher - self.open_orders_watcher: mango.LatestItemObserverSubscriber[mango.OpenOrders] = open_orders_watcher + self.placed_orders_container_watcher: mango.LatestItemObserverSubscriber[ + mango.PlacedOrdersContainer] = placed_orders_container_watcher @property def group(self) -> mango.Group: @@ -55,17 +56,13 @@ class ModelState: return None return self.perp_market_watcher.latest - @property - def open_orders(self) -> mango.OpenOrders: - return self.open_orders_watcher.latest - @property def price(self) -> mango.Price: return self.price_watcher.latest @property - def placed_orders(self) -> typing.Sequence[mango.PlacedOrder]: - return self.open_orders.placed_orders + def existing_orders(self) -> typing.Sequence[mango.PlacedOrder]: + return self.placed_orders_container_watcher.latest.placed_orders def __str__(self) -> str: return f"""« 𝙼𝚘𝚍𝚎𝚕𝚂𝚝𝚊𝚝𝚎 for market '{self.market.symbol}' »""" diff --git a/mango/marketmaking/ordertracker.py b/mango/marketmaking/ordertracker.py index 9d37f60..2bc275f 100644 --- a/mango/marketmaking/ordertracker.py +++ b/mango/marketmaking/ordertracker.py @@ -37,17 +37,17 @@ class OrderTracker: def existing_orders(self, model_state: ModelState) -> typing.Sequence[mango.Order]: live_orders: typing.List[mango.Order] = [] - for placed_order in model_state.placed_orders: - details = self._find_tracked(placed_order.client_id) + for existing_order in model_state.existing_orders: + details = self._find_tracked(existing_order.client_id) if details is None: - self.logger.warning(f"Could not find existing order with client ID {placed_order.client_id}") + self.logger.warning(f"Could not find existing order with client ID {existing_order.client_id}") # Return a stub order so that the Reconciler has the chance to cancel it. - stub = mango.Order.from_ids(placed_order.id, placed_order.client_id, placed_order.side) + stub = mango.Order.from_ids(existing_order.id, existing_order.client_id, existing_order.side) live_orders += [stub] else: - if details.id != placed_order.id: + if details.id != existing_order.id: self.tracked.remove(details) - details = details.with_id(placed_order.id) + details = details.with_id(existing_order.id) self.tracked += [details] live_orders += [details] diff --git a/mango/openorders.py b/mango/openorders.py index b86cd3a..7b0ccf4 100644 --- a/mango/openorders.py +++ b/mango/openorders.py @@ -21,7 +21,6 @@ from pyserum.open_orders_account import OpenOrdersAccount from solana.publickey import PublicKey from solana.rpc.types import MemcmpOpts -from .account import Account from .accountflags import AccountFlags from .accountinfo import AccountInfo from .addressableaccount import AddressableAccount @@ -29,36 +28,9 @@ from .context import Context from .encoding import encode_key from .group import Group from .layouts import layouts -from .orders import Side -from .perpmarket import PerpMarket +from .placedorder import PlacedOrder from .version import Version - -class PlacedOrder(typing.NamedTuple): - id: int - client_id: int - side: Side - - @staticmethod - def build_from_open_orders_data(free_slot_bits: Decimal, is_bid_bits: Decimal, order_ids: typing.Sequence[Decimal], client_order_ids: typing.Sequence[Decimal]): - int_free_slot_bits = int(free_slot_bits) - int_is_bid_bits = int(is_bid_bits) - placed_orders: typing.List[PlacedOrder] = [] - for index in range(len(order_ids)): - if not (int_free_slot_bits & (1 << index)): - order_id = int(order_ids[index]) - client_id = int(client_order_ids[index]) - side = Side.BUY if int_is_bid_bits & (1 << index) else Side.SELL - placed_orders += [PlacedOrder(id=order_id, client_id=client_id, side=side)] - return placed_orders - - def __repr__(self) -> str: - return f"{self}" - - def __str__(self) -> str: - return f"« 𝙿𝚕𝚊𝚌𝚎𝚍𝙾𝚛𝚍𝚎𝚛 {self.side} [{self.id}] {self.client_id} »" - - # # 🥭 OpenOrders class # @@ -107,18 +79,6 @@ class OpenOrders(AddressableAccount): layout.owner, base_token_free, base_token_total, quote_token_free, quote_token_total, placed_orders, layout.referrer_rebate_accrued) - @staticmethod - def from_perps_account_layout(context: Context, account: Account, perp_market: PerpMarket, perp_open_orders: layouts.PERP_OPEN_ORDERS) -> "OpenOrders": - account_flags = AccountFlags(Version.UNSPECIFIED, True, False, - True, False, False, False, False, False) - placed_orders = PlacedOrder.build_from_open_orders_data( - perp_open_orders.free_slot_bits, perp_open_orders.is_bid_bits, perp_open_orders.orders, perp_open_orders.client_order_ids) - open_orders = OpenOrders(account.account_info, Version.V1, context.program_id, - account_flags, perp_market.address, account.address, - Decimal(0), Decimal(0), Decimal(0), Decimal(0), - placed_orders, Decimal(0)) - return open_orders - @staticmethod def parse(account_info: AccountInfo, base_decimals: Decimal, quote_decimals: Decimal) -> "OpenOrders": data = account_info.data diff --git a/mango/perpaccount.py b/mango/perpaccount.py new file mode 100644 index 0000000..5208ce2 --- /dev/null +++ b/mango/perpaccount.py @@ -0,0 +1,59 @@ +# # ⚠ 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) + + +from decimal import Decimal + +from .layouts import layouts +from .perpopenorders import PerpOpenOrders + + +# # 🥭 PerpAccount class +# +# Perp accounts aren't directly addressable. They exist as a sub-object of a full Mango `Account` object. +# +class PerpAccount: + def __init__(self, base_position: Decimal, quote_position: Decimal, long_settled_funding: Decimal, + short_settled_funding: Decimal, mngo_accrued: Decimal, open_orders: PerpOpenOrders): + self.base_position: Decimal = base_position + self.quote_position: Decimal = quote_position + self.long_settled_funding: Decimal = long_settled_funding + self.short_settled_funding: Decimal = short_settled_funding + self.mngo_accrued: Decimal = mngo_accrued + self.open_orders: PerpOpenOrders = open_orders + + @staticmethod + def from_layout(layout: layouts.PERP_ACCOUNT) -> "PerpAccount": + base_position: Decimal = layout.base_position + quote_position: Decimal = layout.quote_position + long_settled_funding: Decimal = layout.long_settled_funding + short_settled_funding: Decimal = layout.short_settled_funding + mngo_accrued: Decimal = layout.mngo_accrued + + open_orders: PerpOpenOrders = PerpOpenOrders.from_layout(layout.open_orders) + + return PerpAccount(base_position, quote_position, long_settled_funding, short_settled_funding, mngo_accrued, open_orders) + + def __str__(self) -> str: + open_orders = f"{self.open_orders}".replace("\n", "\n ") + return f"""« 𝙿𝚎𝚛𝚙𝙰𝚌𝚌𝚘𝚞𝚗𝚝 + Base Position: {self.base_position} + Quote Position: {self.quote_position} + Long Settled Funding: {self.long_settled_funding} + Short Settled Funding: {self.short_settled_funding} + MNGO Accrued: {self.mngo_accrued} + OpenOrders: + {open_orders} +»""" diff --git a/mango/perpopenorders.py b/mango/perpopenorders.py new file mode 100644 index 0000000..f1446d4 --- /dev/null +++ b/mango/perpopenorders.py @@ -0,0 +1,55 @@ +# # ⚠ 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 typing + +from decimal import Decimal + +from .layouts import layouts +from .openorders import PlacedOrder + + +# # 🥭 PerpOpenOrders class +# +class PerpOpenOrders: + def __init__(self, bids_quantity: Decimal, asks_quantity: Decimal, free_slot_bits: Decimal, + is_bid_bits: Decimal, placed_orders: typing.Sequence[PlacedOrder]): + self.bids_quantity: Decimal = bids_quantity + self.asks_quantity: Decimal = asks_quantity + self.free_slot_bits: Decimal = free_slot_bits + self.is_bid_bits: Decimal = is_bid_bits + self.placed_orders: typing.Sequence[PlacedOrder] = placed_orders + + @staticmethod + def from_layout(layout: layouts.PERP_OPEN_ORDERS) -> "PerpOpenOrders": + bids_quantity: Decimal = layout.bids_quantity + asks_quantity: Decimal = layout.asks_quantity + free_slot_bits: Decimal = layout.free_slot_bits + is_bid_bits: Decimal = layout.is_bid_bits + + placed_orders = PlacedOrder.build_from_open_orders_data( + layout.free_slot_bits, layout.is_bid_bits, layout.orders, layout.client_order_ids) + return PerpOpenOrders(bids_quantity, asks_quantity, free_slot_bits, is_bid_bits, placed_orders) + + def __str__(self) -> str: + placed_orders = "\n ".join(map(str, self.placed_orders)) or "None" + + return f"""« 𝙿𝚎𝚛𝚙𝙾𝚙𝚎𝚗𝙾𝚛𝚍𝚎𝚛𝚜 + Bids Quantity: {self.bids_quantity} + Asks Quantity: {self.asks_quantity} + Orders: + {placed_orders} +»""" diff --git a/mango/placedorder.py b/mango/placedorder.py new file mode 100644 index 0000000..b4f755d --- /dev/null +++ b/mango/placedorder.py @@ -0,0 +1,65 @@ +# # ⚠ 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 typing + +from decimal import Decimal + +from .orders import Side + +# # 🥭 PlacedOrder tuple +# +# A `PlacedOrder` is a representation of all the data available from an Open Orders account pertaining to a +# particular order. +# +# The information is usually split across 3 collections - 'is bid', 'orders' and 'client ID's. That can be a +# little awkward to use, so this tuple packages it all together, per order. +# + + +class PlacedOrder(typing.NamedTuple): + id: int + client_id: int + side: Side + + @staticmethod + def build_from_open_orders_data(free_slot_bits: Decimal, is_bid_bits: Decimal, order_ids: typing.Sequence[Decimal], client_order_ids: typing.Sequence[Decimal]): + int_free_slot_bits = int(free_slot_bits) + int_is_bid_bits = int(is_bid_bits) + placed_orders: typing.List[PlacedOrder] = [] + for index in range(len(order_ids)): + if not (int_free_slot_bits & (1 << index)): + order_id = int(order_ids[index]) + client_id = int(client_order_ids[index]) + side = Side.BUY if int_is_bid_bits & (1 << index) else Side.SELL + placed_orders += [PlacedOrder(id=order_id, client_id=client_id, side=side)] + return placed_orders + + def __repr__(self) -> str: + return f"{self}" + + def __str__(self) -> str: + return f"« 𝙿𝚕𝚊𝚌𝚎𝚍𝙾𝚛𝚍𝚎𝚛 {self.side} [{self.id}] {self.client_id} »" + + +# # 🥭 PlacedOrdersContainer protocol +# +# The `PlacedOrdersContainer` protocol exposes commonality between the regular Serum `OpenOrders` class and the +# internally-different `PerpOpenOrders` class. Both have their own `placed_orders` member, but are otherwise +# different enough that a common abstract base class would be a bit kludgy. +# +class PlacedOrdersContainer(typing.Protocol): + placed_orders: typing.Sequence[PlacedOrder]