Added OrderBook instead of passing around bids and asks. Added it to ModelState too, and cleaned up usage.

This commit is contained in:
Geoff Taylor 2021-10-26 18:45:04 +01:00
parent c2615218e3
commit 56599a1037
23 changed files with 290 additions and 197 deletions

View File

@ -1,6 +1,6 @@
FROM python:3.9-buster
RUN sh -c "$(curl -sSfL https://release.solana.com/v1.8.0/install)"
RUN sh -c "$(curl -sSfL https://release.solana.com/v1.8.1/install)"
RUN apt-get update && apt-get -y install bc curl zlib1g-dev

View File

@ -26,13 +26,7 @@ wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
group = mango.Group.load(context, context.group_address)
account = mango.Account.load_for_owner_by_address(context, wallet.address, group, args.account_address)
market_symbol = args.market.upper()
market = context.market_lookup.find_by_symbol(market_symbol)
if market is None:
raise Exception(f"Could not find market {market_symbol}")
market = mango.load_market_by_symbol(context, args.market)
market_operations = mango.create_market_operations(context, wallet, account, market, args.dry_run)
orders = market_operations.load_orders()
print(f"{len(orders)} order(s) to show.")
for order in orders:
print(order)
orderbook = market.fetch_orderbook(context)
print(orderbook)

View File

@ -45,7 +45,7 @@ from .observables import DisposePropagator, DisposeWrapper, NullObserverSubscrib
from .openorders import OpenOrders
from .oracle import OracleSource, Price, Oracle, OracleProvider, SupportedOracleFeature
from .orderbookside import OrderBookSideType, PerpOrderBookSide
from .orders import Order, OrderType, Side
from .orders import Order, OrderType, Side, OrderBook
from .ownedtokenvalue import OwnedTokenValue
from .oraclefactory import create_oracle_provider
from .parse_account_info_to_orders import parse_account_info_to_orders
@ -83,7 +83,7 @@ from .version import Version
from .wallet import Wallet
from .walletbalancer import TargetBalance, FixedTargetBalance, PercentageTargetBalance, parse_target_balance, parse_fixed_target_balance, sort_changes_for_trades, calculate_required_balance_changes, FilterSmallChanges, WalletBalancer, NullWalletBalancer, LiveWalletBalancer, LiveAccountBalancer
from .watcher import Watcher, ManualUpdateWatcher, LamdaUpdateWatcher
from .watchers import build_group_watcher, build_account_watcher, build_cache_watcher, build_spot_open_orders_watcher, build_serum_open_orders_watcher, build_perp_open_orders_watcher, build_price_watcher, build_serum_inventory_watcher, build_perp_orderbook_side_watcher, build_serum_orderbook_side_watcher
from .watchers import build_group_watcher, build_account_watcher, build_cache_watcher, build_spot_open_orders_watcher, build_serum_open_orders_watcher, build_perp_open_orders_watcher, build_price_watcher, build_serum_inventory_watcher, build_orderbook_watcher
from .websocketsubscription import WebSocketSubscription, WebSocketProgramSubscription, WebSocketAccountSubscription, WebSocketLogSubscription, WebSocketSubscriptionManager, IndividualWebSocketSubscriptionManager, SharedWebSocketSubscriptionManager
from .layouts import layouts

View File

@ -200,6 +200,28 @@ class Account(AddressableAccount):
raise Exception(f"Account account not found at address '{address}'")
return Account.parse(account_info, group)
@staticmethod
def load_all(context: Context, group: Group) -> typing.Sequence["Account"]:
# mango_group is just after the METADATA, which is the first entry.
group_offset = layouts.METADATA.sizeof()
# owner is just after mango_group in the layout, and it's a PublicKey which is 32 bytes.
filters = [
MemcmpOpts(
offset=group_offset,
bytes=encode_key(group.address)
)
]
results = context.client.get_program_accounts(
context.mango_program_address, memcmp_opts=filters, data_size=layouts.MANGO_ACCOUNT.sizeof())
accounts = []
for account_data in results:
address = PublicKey(account_data["pubkey"])
account_info = AccountInfo._from_response_values(account_data["account"], address)
account = Account.parse(account_info, group)
accounts += [account]
return accounts
@staticmethod
def load_all_for_owner(context: Context, owner: PublicKey, group: Group) -> typing.Sequence["Account"]:
# mango_group is just after the METADATA, which is the first entry.
@ -217,7 +239,8 @@ class Account(AddressableAccount):
)
]
results = context.client.get_program_accounts(context.mango_program_address, memcmp_opts=filters)
results = context.client.get_program_accounts(
context.mango_program_address, memcmp_opts=filters, data_size=layouts.MANGO_ACCOUNT.sizeof())
accounts = []
for account_data in results:
address = PublicKey(account_data["pubkey"])

View File

@ -13,15 +13,15 @@
# [Github](https://github.com/blockworks-foundation)
# [Email](mailto:hello@blockworks.foundation)
import typing
from solana.publickey import PublicKey
from .accountinfo import AccountInfo
from .context import Context
from .lotsizeconverter import LotSizeConverter
from .market import Market, InventorySource
from .orders import Order
from .orders import Order, OrderBook
from .token import Token
@ -33,5 +33,22 @@ class LoadedMarket(Market):
def __init__(self, program_address: PublicKey, address: PublicKey, inventory_source: InventorySource, base: Token, quote: Token, lot_size_converter: LotSizeConverter):
super().__init__(program_address, address, inventory_source, base, quote, lot_size_converter)
def orders(self, context: Context) -> typing.Sequence[Order]:
raise NotImplementedError("LoadedMarket.orders() is not implemented on the base type.")
@property
def bids_address(self) -> PublicKey:
raise NotImplementedError("LoadedMarket.bids_address() is not implemented on the base type.")
@property
def asks_address(self) -> PublicKey:
raise NotImplementedError("LoadedMarket.asks_address() is not implemented on the base type.")
def parse_account_info_to_orders(self, account_info: AccountInfo) -> typing.Sequence[Order]:
raise NotImplementedError("LoadedMarket.parse_account_info_to_orders() is not implemented on the base type.")
def parse_account_infos_to_orderbook(self, bids_account_info: AccountInfo, asks_account_info: AccountInfo) -> OrderBook:
bids_orderbook = self.parse_account_info_to_orders(bids_account_info)
asks_orderbook = self.parse_account_info_to_orders(asks_account_info)
return OrderBook(self.symbol, bids_orderbook, asks_orderbook)
def fetch_orderbook(self, context: Context) -> OrderBook:
[bids_info, asks_info] = AccountInfo.load_multiple(context, [self.bids_address, self.asks_address])
return self.parse_account_infos_to_orderbook(bids_info, asks_info)

View File

@ -87,19 +87,17 @@ 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, bids: typing.Sequence[mango.Order],
asks: typing.Sequence[mango.Order]) -> ModelState:
inventory: mango.Inventory, orderbook: mango.OrderBook) -> 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)
placed_orders_container_watcher: mango.ManualUpdateWatcher[
mango.PlacedOrdersContainer] = mango.ManualUpdateWatcher(placed_orders_container)
inventory_watcher: mango.ManualUpdateWatcher[mango.Inventory] = mango.ManualUpdateWatcher(inventory)
bids_watcher: mango.ManualUpdateWatcher[typing.Sequence[mango.Order]] = mango.ManualUpdateWatcher(bids)
asks_watcher: mango.ManualUpdateWatcher[typing.Sequence[mango.Order]] = mango.ManualUpdateWatcher(asks)
orderbook_watcher: mango.ManualUpdateWatcher[mango.OrderBook] = mango.ManualUpdateWatcher(orderbook)
return ModelState(order_owner, market, group_watcher, account_watcher, price_watcher,
placed_orders_container_watcher, inventory_watcher, bids_watcher, asks_watcher)
placed_orders_container_watcher, inventory_watcher, orderbook_watcher)
def __str__(self) -> str:
return "« 𝙿𝚘𝚕𝚕𝚒𝚗𝚐𝙼𝚘𝚍𝚎𝚕𝚂𝚝𝚊𝚝𝚎𝙱𝚞𝚒𝚕𝚍𝚎𝚛 »"
@ -138,8 +136,8 @@ class SerumPollingModelStateBuilder(PollingModelStateBuilder):
self.open_orders_address,
self.base_inventory_token_account.address,
self.quote_inventory_token_account.address,
self.market.underlying_serum_market.state.bids(),
self.market.underlying_serum_market.state.asks()
self.market.bids_address,
self.market.asks_address
]
account_infos: typing.Sequence[mango.AccountInfo] = mango.AccountInfo.load_multiple(context, addresses)
group: mango.Group = mango.Group.parse(context, account_infos[0])
@ -156,11 +154,7 @@ class SerumPollingModelStateBuilder(PollingModelStateBuilder):
quote_inventory_token_account = mango.TokenAccount.parse(
account_infos[4], self.quote_inventory_token_account.value.token)
# Both these will have top-of-book at index 0.
bids: typing.Sequence[mango.Order] = mango.parse_account_info_to_orders(
account_infos[5], self.market.underlying_serum_market)
asks: typing.Sequence[mango.Order] = mango.parse_account_info_to_orders(
account_infos[6], self.market.underlying_serum_market)
orderbook: mango.OrderBook = self.market.parse_account_infos_to_orderbook(account_infos[5], account_infos[6])
price: mango.Price = self.oracle.fetch_price(context)
@ -173,7 +167,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, bids, asks)
return self.from_values(self.order_owner, self.market, group, account, price, placed_orders_container, inventory, orderbook)
def __str__(self) -> str:
return f"""« 𝚂𝚎𝚛𝚞𝚖𝙿𝚘𝚕𝚕𝚒𝚗𝚐𝙼𝚘𝚍𝚎𝚕𝚂𝚝𝚊𝚝𝚎𝙱𝚞𝚒𝚕𝚍𝚎𝚛 for market '{self.market.symbol}' »"""
@ -212,8 +206,8 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder):
self.group_address,
self.cache_address,
self.account_address,
self.market.underlying_serum_market.state.bids(),
self.market.underlying_serum_market.state.asks(),
self.market.bids_address,
self.market.asks_address,
*self.all_open_orders_addresses
]
account_infos: typing.Sequence[mango.AccountInfo] = mango.AccountInfo.load_multiple(context, addresses)
@ -254,15 +248,11 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder):
base_value,
quote_value)
# Both these will have top-of-book at index 0.
bids: typing.Sequence[mango.Order] = mango.parse_account_info_to_orders(
account_infos[3], self.market.underlying_serum_market)
asks: typing.Sequence[mango.Order] = mango.parse_account_info_to_orders(
account_infos[4], self.market.underlying_serum_market)
orderbook: mango.OrderBook = self.market.parse_account_infos_to_orderbook(account_infos[3], account_infos[4])
price: mango.Price = self.oracle.fetch_price(context)
return self.from_values(self.order_owner, self.market, group, account, price, placed_orders_container, inventory, bids, asks)
return self.from_values(self.order_owner, self.market, group, account, price, placed_orders_container, inventory, orderbook)
def __str__(self) -> str:
return f"""« 𝚂𝚙𝚘𝚝𝙿𝚘𝚕𝚕𝚒𝚗𝚐𝙼𝚘𝚍𝚎𝚕𝚂𝚝𝚊𝚝𝚎𝙱𝚞𝚒𝚕𝚍𝚎𝚛 for market '{self.market.symbol}' »"""
@ -322,15 +312,11 @@ class PerpPollingModelStateBuilder(PollingModelStateBuilder):
base_token_value,
quote_token_value)
# Both these will have top-of-book at index 0.
bids: mango.PerpOrderBookSide = mango.PerpOrderBookSide.parse(
context, account_infos[3], self.market.underlying_perp_market)
asks: mango.PerpOrderBookSide = mango.PerpOrderBookSide.parse(
context, account_infos[4], self.market.underlying_perp_market)
orderbook: mango.OrderBook = self.market.parse_account_infos_to_orderbook(account_infos[3], account_infos[4])
price: mango.Price = self.oracle.fetch_price(context)
return self.from_values(self.order_owner, self.market, group, account, price, placed_orders_container, inventory, bids.orders(), asks.orders())
return self.from_values(self.order_owner, self.market, group, account, price, placed_orders_container, inventory, orderbook)
def __str__(self) -> str:
return f"""« 𝙿𝚎𝚛𝚙𝙿𝚘𝚕𝚕𝚒𝚗𝚐𝙼𝚘𝚍𝚎𝚕𝚂𝚝𝚊𝚝𝚎𝙱𝚞𝚒𝚕𝚍𝚎𝚛 for market '{self.market.symbol}' »"""

View File

@ -129,10 +129,8 @@ 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_bids_watcher: mango.Watcher[typing.Sequence[mango.Order]] = mango.build_serum_orderbook_side_watcher(
context, websocket_manager, health_check, market.underlying_serum_market, mango.OrderBookSideType.BIDS)
latest_asks_watcher: mango.Watcher[typing.Sequence[mango.Order]] = mango.build_serum_orderbook_side_watcher(
context, websocket_manager, health_check, market.underlying_serum_market, mango.OrderBookSideType.ASKS)
latest_orderbook_watcher = mango.build_orderbook_watcher(
context, websocket_manager, health_check, market)
elif isinstance(market, mango.SpotMarket):
market_index: int = group.find_spot_market_index(market.address)
order_owner = account.spot_open_orders[market_index] or SYSTEM_PROGRAM_ADDRESS
@ -157,10 +155,8 @@ def _websocket_model_state_builder_factory(context: mango.Context, disposer: man
inventory_watcher = mango.SpotInventoryAccountWatcher(
market, latest_account_observer, latest_group_observer, all_open_orders_watchers, cache_watcher)
latest_bids_watcher = mango.build_serum_orderbook_side_watcher(
context, websocket_manager, health_check, market.underlying_serum_market, mango.OrderBookSideType.BIDS)
latest_asks_watcher = mango.build_serum_orderbook_side_watcher(
context, websocket_manager, health_check, market.underlying_serum_market, mango.OrderBookSideType.ASKS)
latest_orderbook_watcher = mango.build_orderbook_watcher(
context, websocket_manager, health_check, market)
elif isinstance(market, mango.PerpMarket):
order_owner = account.address
cache = mango.Cache.load(context, group.cache)
@ -169,14 +165,12 @@ def _websocket_model_state_builder_factory(context: mango.Context, disposer: man
market, latest_account_observer, latest_group_observer, cache_watcher, group)
latest_open_orders_observer = mango.build_perp_open_orders_watcher(
context, websocket_manager, health_check, market, account, group, account_subscription)
latest_bids_watcher = mango.build_perp_orderbook_side_watcher(
context, websocket_manager, health_check, market, mango.OrderBookSideType.BIDS)
latest_asks_watcher = mango.build_perp_orderbook_side_watcher(
context, websocket_manager, health_check, market, mango.OrderBookSideType.ASKS)
latest_orderbook_watcher = mango.build_orderbook_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, latest_group_observer, latest_account_observer,
latest_price_observer, latest_open_orders_observer,
inventory_watcher, latest_bids_watcher, latest_asks_watcher)
inventory_watcher, latest_orderbook_watcher)
return WebsocketModelStateBuilder(model_state)

View File

@ -23,7 +23,7 @@ from solana.publickey import PublicKey
from .constants import SYSTEM_PROGRAM_ADDRESS
from .market import Market, DryRunMarket
from .orders import Order
from .orders import Order, OrderBook
# # 🥭 MarketOperations
@ -65,7 +65,7 @@ class MarketOperations(metaclass=abc.ABCMeta):
raise NotImplementedError("MarketOperations.place_order() is not implemented on the base type.")
@abc.abstractmethod
def load_orders(self) -> typing.Sequence[Order]:
def load_orderbook(self) -> OrderBook:
raise NotImplementedError("MarketOperations.load_orders() is not implemented on the base type.")
@abc.abstractmethod
@ -110,8 +110,8 @@ class DryRunMarketOperations(MarketOperations):
self.logger.info(f"[Dry Run] Not placing order {order}.")
return order
def load_orders(self) -> typing.Sequence[Order]:
return []
def load_orderbook(self) -> OrderBook:
return OrderBook(self.market_name, [], [])
def load_my_orders(self) -> typing.Sequence[Order]:
return []

View File

@ -25,7 +25,7 @@ from .group import Group
from .inventory import Inventory
from .market import Market
from .oracle import Price
from .orders import Order
from .orders import Order, OrderBook
from .placedorder import PlacedOrdersContainer
from .watcher import Watcher
@ -43,8 +43,7 @@ class ModelState:
price_watcher: Watcher[Price],
placed_orders_container_watcher: Watcher[PlacedOrdersContainer],
inventory_watcher: Watcher[Inventory],
bids: Watcher[typing.Sequence[Order]],
asks: Watcher[typing.Sequence[Order]]
orderbook: Watcher[OrderBook]
):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.order_owner: PublicKey = order_owner
@ -55,8 +54,7 @@ class ModelState:
self.placed_orders_container_watcher: Watcher[
PlacedOrdersContainer] = placed_orders_container_watcher
self.inventory_watcher: Watcher[Inventory] = inventory_watcher
self.bids_watcher: Watcher[typing.Sequence[Order]] = bids
self.asks_watcher: Watcher[typing.Sequence[Order]] = asks
self.orderbook_watcher: Watcher[OrderBook] = orderbook
self.not_quoting: bool = False
self.state: typing.Dict[str, typing.Any] = {}
@ -81,43 +79,35 @@ class ModelState:
def inventory(self) -> Inventory:
return self.inventory_watcher.latest
@property
def orderbook(self) -> OrderBook:
return self.orderbook_watcher.latest
@property
def bids(self) -> typing.Sequence[Order]:
return self.bids_watcher.latest
return self.orderbook.bids
@property
def asks(self) -> typing.Sequence[Order]:
return self.asks_watcher.latest
return self.orderbook.asks
# The top bid is the highest price someone is willing to pay to BUY
@property
def top_bid(self) -> typing.Optional[Order]:
if self.bids_watcher.latest and len(self.bids_watcher.latest) > 0:
# Top-of-book is always at index 0 for us.
return self.bids_watcher.latest[0]
else:
return None
return self.orderbook.top_bid
# The top ask is the lowest price someone is willing to pay to SELL
@property
def top_ask(self) -> typing.Optional[Order]:
if self.asks_watcher.latest and len(self.asks_watcher.latest) > 0:
# Top-of-book is always at index 0 for us.
return self.asks_watcher.latest[0]
else:
return None
return self.orderbook.top_ask
@property
def spread(self) -> Decimal:
top_ask = self.top_ask
top_bid = self.top_bid
if top_ask is None or top_bid is None:
return Decimal(0)
else:
return top_ask.price - top_bid.price
return self.orderbook.spread
def current_orders(self) -> typing.Sequence[Order]:
all_orders = [*self.bids_watcher.latest, *self.asks_watcher.latest]
self.orderbook
all_orders = [*self.bids, *self.asks]
return list([o for o in all_orders if o.owner == self.order_owner])
def __str__(self) -> str:
@ -127,8 +117,8 @@ class ModelState:
Price: {self.price_watcher.latest}
Inventory: {self.inventory_watcher.latest}
Existing Order Count: {len(self.placed_orders_container_watcher.latest.placed_orders)}
Bid Count: {len(self.bids_watcher.latest)}
Ask Count: {len(self.bids_watcher.latest)}
Bid Count: {len(self.bids)}
Ask Count: {len(self.asks)}
»"""
def __repr__(self) -> str:

View File

@ -27,7 +27,7 @@ from ...loadedmarket import LoadedMarket
from ...market import Market
from ...observables import observable_pipeline_error_reporter
from ...oracle import Oracle, OracleProvider, OracleSource, Price, SupportedOracleFeature
from ...orders import Order, Side
from ...orders import OrderBook
# # 🥭 Market
@ -59,10 +59,18 @@ class MarketOracle(Oracle):
self.source: OracleSource = OracleSource("Market", name, features, market)
def fetch_price(self, context: Context) -> Price:
orders: typing.Sequence[Order] = self.loaded_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
orderbook: OrderBook = self.loaded_market.fetch_orderbook(context)
if orderbook.top_bid is None:
raise Exception(f"[{self.source}] Cannot determine complete price data - no top bid")
top_bid = orderbook.top_bid.price
if orderbook.top_ask is None:
raise Exception(f"[{self.source}] Cannot determine complete price data - no top bid")
top_ask = orderbook.top_ask.price
if orderbook.mid_price is None:
raise Exception(f"[{self.source}] Cannot determine complete price data - no mid price")
mid_price = orderbook.mid_price
return Price(self.source, datetime.now(), self.market, top_bid, mid_price, top_ask, MarketOracleConfidence)

View File

@ -80,7 +80,7 @@ class PerpOrderBookSide(AddressableAccount):
return PerpOrderBookSide(account_info, version, meta_data, perp_market_details, bump_index, free_list_len, free_list_head, root_node, leaf_count, nodes)
@staticmethod
def parse(context: Context, account_info: AccountInfo, perp_market_details: PerpMarketDetails) -> "PerpOrderBookSide":
def parse(account_info: AccountInfo, perp_market_details: PerpMarketDetails) -> "PerpOrderBookSide":
data = account_info.data
if len(data) != layouts.ORDERBOOK_SIDE.sizeof():
raise Exception(
@ -94,7 +94,7 @@ class PerpOrderBookSide(AddressableAccount):
account_info = AccountInfo.load(context, address)
if account_info is None:
raise Exception(f"PerpOrderBookSide account not found at address '{address}'")
return PerpOrderBookSide.parse(context, account_info, perp_market_details)
return PerpOrderBookSide.parse(account_info, perp_market_details)
def orders(self) -> typing.Sequence[Order]:
if self.leaf_count == 0:

View File

@ -189,3 +189,76 @@ class Order(typing.NamedTuple):
def __repr__(self) -> str:
return f"{self}"
class OrderBook:
def __init__(self, symbol: str, bids: typing.Sequence[Order], asks: typing.Sequence[Order]):
self.symbol: str = symbol
# Sort bids high to low, so best bid is at index 0
bids_list: typing.List[Order] = list(bids)
bids_list.sort(key=lambda order: order.price, reverse=True)
self.bids: typing.Sequence[Order] = bids_list
# Sort bids low to high, so best bid is at index 0
asks_list: typing.List[Order] = list(asks)
asks_list.sort(key=lambda order: order.price)
self.asks: typing.Sequence[Order] = asks_list
# The top bid is the highest price someone is willing to pay to BUY
@property
def top_bid(self) -> typing.Optional[Order]:
if self.bids and len(self.bids) > 0:
# Top-of-book is always at index 0 for us.
return self.bids[0]
return None
# The top ask is the lowest price someone is willing to pay to SELL
@property
def top_ask(self) -> typing.Optional[Order]:
if self.asks and len(self.asks) > 0:
# Top-of-book is always at index 0 for us.
return self.asks[0]
return None
# The mid price is halfway between the best bid and best ask.
@property
def mid_price(self) -> typing.Optional[Decimal]:
if self.top_bid is not None and self.top_ask is not None:
return (self.top_bid.price + self.top_ask.price) / 2
elif self.top_bid is not None:
return self.top_bid.price
elif self.top_ask is not None:
return self.top_ask.price
return None
@property
def spread(self) -> Decimal:
top_ask = self.top_ask
top_bid = self.top_bid
if top_ask is None or top_bid is None:
return Decimal(0)
else:
return top_ask.price - top_bid.price
def __str__(self) -> str:
def _order_to_str(order: Order):
quantity = f"{order.quantity:,.8f}"
price = f"{order.price:,.8f}"
return f"{order.side} {quantity:>20} at {price:>20}"
orders_to_show = 5
lines = []
for counter in range(orders_to_show):
bid = _order_to_str(self.bids[counter]) if len(self.bids) > counter else ""
ask = _order_to_str(self.asks[counter]) if len(self.asks) > counter else ""
lines += [f"{bid:50} :: {ask}"]
text = "\n\t".join(lines)
spread_description = "N/A"
if self.spread != 0 and self.top_bid is not None:
spread_percentage = (self.spread / self.top_bid.price)
spread_description = f"{self.spread:,.8f}, {spread_percentage:,.3%}"
return f"« 𝙾𝚛𝚍𝚎𝚛𝙱𝚘𝚘𝚔 {self.symbol} [spread: {spread_description}]\n\t{text}\n»"
def __repr__(self) -> str:
return f"{self}"

View File

@ -54,6 +54,18 @@ class PerpMarket(LoadedMarket):
def group(self) -> Group:
return self.underlying_perp_market.group
@property
def bids_address(self) -> PublicKey:
return self.underlying_perp_market.bids
@property
def asks_address(self) -> PublicKey:
return self.underlying_perp_market.asks
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()
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)
@ -77,14 +89,6 @@ class PerpMarket(LoadedMarket):
distinct.sort(key=lambda address: address._key or [0])
return distinct
def orders(self, context: Context) -> typing.Sequence[Order]:
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])
bid_side = PerpOrderBookSide.parse(context, bids, self.underlying_perp_market)
ask_side = PerpOrderBookSide.parse(context, asks, self.underlying_perp_market)
return [*bid_side.orders(), *ask_side.orders()]
def observe_events(self, context: Context, interval: int = 30) -> DisposingSubject:
perp_event_queue: PerpEventQueue = PerpEventQueue.load(
context, self.underlying_perp_market.event_queue, self.lot_size_converter)

View File

@ -24,7 +24,7 @@ from .combinableinstructions import CombinableInstructions
from .constants import SYSTEM_PROGRAM_ADDRESS
from .context import Context
from .marketoperations import MarketOperations
from .orders import Order
from .orders import Order, OrderBook
from .perpmarketinstructionbuilder import PerpMarketInstructionBuilder
from .perpmarket import PerpMarket
from .wallet import Wallet
@ -86,12 +86,12 @@ class PerpMarketOperations(MarketOperations):
def ensure_openorders(self) -> PublicKey:
return SYSTEM_PROGRAM_ADDRESS
def load_orders(self) -> typing.Sequence[Order]:
return self.perp_market.orders(self.context)
def load_orderbook(self) -> OrderBook:
return self.perp_market.fetch_orderbook(self.context)
def load_my_orders(self) -> typing.Sequence[Order]:
all_orders = self.perp_market.orders(self.context)
return list([o for o in all_orders if o.owner == self.account.address])
orderbook: OrderBook = self.load_orderbook()
return list([o for o in [*orderbook.bids, *orderbook.asks] if o.owner == self.account.address])
def __str__(self) -> str:
return f"""« 𝙿𝚎𝚛𝚙𝚜𝙾𝚛𝚍𝚎𝚛𝙿𝚕𝚊𝚌𝚎𝚛 [{self.market_name}] »"""

View File

@ -13,7 +13,6 @@
# [Github](https://github.com/blockworks-foundation)
# [Email](mailto:hello@blockworks.foundation)
import itertools
import typing
from decimal import Decimal
@ -44,19 +43,22 @@ class SerumMarket(LoadedMarket):
quote_lot_size: Decimal = Decimal(underlying_serum_market.state.quote_lot_size())
self.lot_size_converter: LotSizeConverter = LotSizeConverter(base, base_lot_size, quote, quote_lot_size)
@property
def bids_address(self) -> PublicKey:
return self.underlying_serum_market.state.bids()
@property
def asks_address(self) -> PublicKey:
return self.underlying_serum_market.state.asks()
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())
return event_queue.unprocessed_events
def orders(self, context: Context) -> typing.Sequence[Order]:
raw_market = self.underlying_serum_market
[bids_info, asks_info] = AccountInfo.load_multiple(
context, [raw_market.state.bids(), raw_market.state.asks()])
bids_orderbook = PySerumOrderBook.from_bytes(raw_market.state, bids_info.data)
asks_orderbook = PySerumOrderBook.from_bytes(raw_market.state, asks_info.data)
return list(map(Order.from_serum_order, itertools.chain(bids_orderbook.orders(), asks_orderbook.orders())))
def find_openorders_address_for_owner(self, context: Context, owner: PublicKey) -> typing.Optional[PublicKey]:
all_open_orders = OpenOrders.load_for_market_and_owner(
context, self.address, owner, context.serum_program_address, self.base.decimals, self.quote.decimals)

View File

@ -23,7 +23,7 @@ from .combinableinstructions import CombinableInstructions
from .constants import SYSTEM_PROGRAM_ADDRESS
from .context import Context
from .marketoperations import MarketOperations
from .orders import Order
from .orders import Order, OrderBook
from .serummarket import SerumMarket
from .serummarketinstructionbuilder import SerumMarketInstructionBuilder
from .wallet import Wallet
@ -90,16 +90,16 @@ class SerumMarketOperations(MarketOperations):
return self.market_instruction_builder.open_orders_address
return self.create_openorders()
def load_orders(self) -> typing.Sequence[Order]:
return self.serum_market.orders(self.context)
def load_orderbook(self) -> OrderBook:
return self.serum_market.fetch_orderbook(self.context)
def load_my_orders(self) -> typing.Sequence[Order]:
open_orders_address = self.market_instruction_builder.open_orders_address
if not open_orders_address:
return []
all_orders = self.serum_market.orders(self.context)
return list([o for o in all_orders if o.owner == open_orders_address])
orderbook: OrderBook = self.load_orderbook()
return list([o for o in [*orderbook.bids, *orderbook.asks] if o.owner == open_orders_address])
def _build_crank(self, limit: Decimal = Decimal(32)) -> CombinableInstructions:
open_orders_to_crank: typing.List[PublicKey] = []

View File

@ -13,7 +13,6 @@
# [Github](https://github.com/blockworks-foundation)
# [Email](mailto:hello@blockworks.foundation)
import itertools
import typing
from decimal import Decimal
@ -45,19 +44,22 @@ class SpotMarket(LoadedMarket):
quote_lot_size: Decimal = Decimal(underlying_serum_market.state.quote_lot_size())
self.lot_size_converter: LotSizeConverter = LotSizeConverter(base, base_lot_size, quote, quote_lot_size)
@property
def bids_address(self) -> PublicKey:
return self.underlying_serum_market.state.bids()
@property
def asks_address(self) -> PublicKey:
return self.underlying_serum_market.state.asks()
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())
return event_queue.unprocessed_events
def orders(self, context: Context) -> typing.Sequence[Order]:
raw_market = self.underlying_serum_market
[bids_info, asks_info] = AccountInfo.load_multiple(
context, [raw_market.state.bids(), raw_market.state.asks()])
bids_orderbook = PySerumOrderBook.from_bytes(raw_market.state, bids_info.data)
asks_orderbook = PySerumOrderBook.from_bytes(raw_market.state, asks_info.data)
return list(map(Order.from_serum_order, itertools.chain(bids_orderbook.orders(), asks_orderbook.orders())))
def __str__(self) -> str:
return f"""« 𝚂𝚙𝚘𝚝𝙼𝚊𝚛𝚔𝚎𝚝 {self.symbol} {self.address} [{self.program_address}]
Event Queue: {self.underlying_serum_market.state.event_queue()}

View File

@ -25,7 +25,7 @@ from .constants import SYSTEM_PROGRAM_ADDRESS
from .context import Context
from .group import Group
from .marketoperations import MarketOperations
from .orders import Order
from .orders import Order, OrderBook
from .spotmarket import SpotMarket
from .spotmarketinstructionbuilder import SpotMarketInstructionBuilder
from .wallet import Wallet
@ -102,15 +102,15 @@ class SpotMarketOperations(MarketOperations):
return existing
return self.create_openorders()
def load_orders(self) -> typing.Sequence[Order]:
return self.spot_market.orders(self.context)
def load_orderbook(self) -> OrderBook:
return self.spot_market.fetch_orderbook(self.context)
def load_my_orders(self) -> typing.Sequence[Order]:
if not self.open_orders_address:
return []
all_orders = self.spot_market.orders(self.context)
return list([o for o in all_orders if o.owner == self.open_orders_address])
orderbook: OrderBook = self.load_orderbook()
return list([o for o in [*orderbook.bids, *orderbook.asks] if o.owner == self.open_orders_address])
def _build_crank(self, limit: Decimal = Decimal(32), add_self: bool = False) -> CombinableInstructions:
open_orders_to_crank: typing.List[PublicKey] = []

View File

@ -139,9 +139,11 @@ class ImmediateTradeExecutor(TradeExecutor):
def buy(self, symbol: str, quantity: Decimal) -> Order:
market_operations: MarketOperations = self._build_market_operations(symbol)
orders = market_operations.load_orders()
orderbook = market_operations.load_orderbook()
if orderbook.top_ask is None:
raise Exception(f"Could not determine top ask on {orderbook.symbol}")
top_ask = min([order.price for order in orders if order.side == Side.SELL])
top_ask = orderbook.top_ask.price
increase_factor = Decimal(1) + self.price_adjustment_factor
price = top_ask * increase_factor
@ -152,9 +154,11 @@ class ImmediateTradeExecutor(TradeExecutor):
def sell(self, symbol: str, quantity: Decimal) -> Order:
market_operations: MarketOperations = self._build_market_operations(symbol)
orders = market_operations.load_orders()
orderbook = market_operations.load_orderbook()
if orderbook.top_bid is None:
raise Exception(f"Could not determine top bid on {orderbook.symbol}")
top_bid = max([order.price for order in orders if order.side == Side.BUY])
top_bid = orderbook.top_bid.price
decrease_factor = Decimal(1) - self.price_adjustment_factor
price = top_bid * decrease_factor

View File

@ -18,7 +18,6 @@ import typing
from decimal import Decimal
from pyserum.market import Market as PySerumMarket
from pyserum.market.orderbook import OrderBook as PySerumOrderBook
from solana.publickey import PublicKey
from .account import Account
@ -30,13 +29,13 @@ from .group import Group
from .healthcheck import HealthCheck
from .instructions import build_create_serum_open_orders_instructions
from .inventory import Inventory, InventorySource
from .loadedmarket import LoadedMarket
from .market import Market
from .observables import DisposePropagator, LatestItemObserverSubscriber
from .openorders import OpenOrders
from .oracle import Price
from .oraclefactory import OracleProvider, create_oracle_provider
from .orderbookside import OrderBookSideType, PerpOrderBookSide
from .orders import Order
from .orders import OrderBook
from .perpmarket import PerpMarket
from .placedorder import PlacedOrdersContainer
from .serummarket import SerumMarket
@ -205,45 +204,37 @@ def build_serum_inventory_watcher(context: Context, manager: WebSocketSubscripti
return LamdaUpdateWatcher(serum_inventory_accessor)
def build_perp_orderbook_side_watcher(context: Context, manager: WebSocketSubscriptionManager, health_check: HealthCheck, perp_market: PerpMarket, side: OrderBookSideType) -> Watcher[typing.Sequence[Order]]:
orderbook_address: PublicKey = perp_market.underlying_perp_market.bids if side == OrderBookSideType.BIDS else perp_market.underlying_perp_market.asks
orderbook_side_info = AccountInfo.load(context, orderbook_address)
if orderbook_side_info is None:
raise Exception(f"Could not find perp order book side at address {orderbook_address}.")
initial_orderbook_side: PerpOrderBookSide = PerpOrderBookSide.parse(
context, orderbook_side_info, perp_market.underlying_perp_market)
def build_orderbook_watcher(context: Context, manager: WebSocketSubscriptionManager, health_check: HealthCheck, market: LoadedMarket) -> Watcher[OrderBook]:
orderbook_addresses: typing.List[PublicKey] = [
market.bids_address,
market.asks_address
]
orderbook_infos = AccountInfo.load_multiple(context, orderbook_addresses)
if len(orderbook_infos) != 2 or orderbook_infos[0] is None or orderbook_infos[1] is None:
raise Exception(f"Could not find {market.symbol} order book at addresses {orderbook_addresses}.")
orders_subscription = WebSocketAccountSubscription[typing.Sequence[Order]](
context, orderbook_address, lambda account_info: PerpOrderBookSide.parse(context, account_info, perp_market.underlying_perp_market).orders())
manager.add(orders_subscription)
initial_orderbook: OrderBook = market.parse_account_infos_to_orderbook(orderbook_infos[0], orderbook_infos[1])
updatable_orderbook: OrderBook = market.parse_account_infos_to_orderbook(
orderbook_infos[0], orderbook_infos[1])
latest_orders_observer = LatestItemObserverSubscriber[typing.Sequence[Order]](initial_orderbook_side.orders())
def _update_bids(account_info: AccountInfo) -> OrderBook:
new_bids = market.parse_account_info_to_orders(account_info)
updatable_orderbook.bids = new_bids
return updatable_orderbook
orders_subscription.publisher.subscribe(latest_orders_observer)
health_check.add("orderbook_side_subscription", orders_subscription.publisher)
return latest_orders_observer
def _update_asks(account_info: AccountInfo) -> OrderBook:
new_asks = market.parse_account_info_to_orders(account_info)
updatable_orderbook.asks = new_asks
return updatable_orderbook
bids_subscription = WebSocketAccountSubscription[OrderBook](context, orderbook_addresses[0], _update_bids)
manager.add(bids_subscription)
asks_subscription = WebSocketAccountSubscription[OrderBook](context, orderbook_addresses[1], _update_asks)
manager.add(asks_subscription)
orderbook_observer = LatestItemObserverSubscriber[OrderBook](initial_orderbook)
def build_serum_orderbook_side_watcher(context: Context, manager: WebSocketSubscriptionManager, health_check: HealthCheck, underlying_serum_market: PySerumMarket, side: OrderBookSideType) -> Watcher[typing.Sequence[Order]]:
orderbook_address: PublicKey = underlying_serum_market.state.bids(
) if side == OrderBookSideType.BIDS else underlying_serum_market.state.asks()
orderbook_side_info = AccountInfo.load(context, orderbook_address)
if orderbook_side_info is None:
raise Exception(f"Could not find Serum order book side at address {orderbook_address}.")
def account_info_to_orderbook(account_info: AccountInfo) -> typing.Sequence[Order]:
serum_orderbook_side = PySerumOrderBook.from_bytes(
underlying_serum_market.state, account_info.data)
return list(map(Order.from_serum_order, serum_orderbook_side.orders()))
initial_orderbook_side: typing.Sequence[Order] = account_info_to_orderbook(orderbook_side_info)
orders_subscription = WebSocketAccountSubscription[typing.Sequence[Order]](
context, orderbook_address, account_info_to_orderbook)
manager.add(orders_subscription)
latest_orders_observer = LatestItemObserverSubscriber[typing.Sequence[Order]](initial_orderbook_side)
orders_subscription.publisher.subscribe(latest_orders_observer)
health_check.add("orderbook_side_subscription", orders_subscription.publisher)
return latest_orders_observer
bids_subscription.publisher.subscribe(orderbook_observer)
asks_subscription.publisher.subscribe(orderbook_observer)
health_check.add("orderbook_bids_subscription", bids_subscription.publisher)
health_check.add("orderbook_asks_subscription", asks_subscription.publisher)
return orderbook_observer

View File

@ -144,11 +144,11 @@ def fake_inventory():
def fake_bids():
return None
return []
def fake_asks():
return None
return []
def fake_model_state(order_owner: typing.Optional[PublicKey] = None,
@ -158,8 +158,7 @@ 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,
bids: typing.Optional[typing.Sequence[mango.Order]] = None,
asks: typing.Optional[typing.Sequence[mango.Order]] = None) -> mango.ModelState:
orderbook: typing.Optional[mango.OrderBook] = 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()
@ -167,17 +166,15 @@ def fake_model_state(order_owner: typing.Optional[PublicKey] = None,
price = price or fake_price()
placed_orders_container = placed_orders_container or fake_placed_orders_container()
inventory = inventory or fake_inventory()
bids = bids or fake_bids()
asks = asks or fake_asks()
orderbook = orderbook or mango.OrderBook("FAKE", fake_bids(), fake_asks())
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)
placed_orders_container_watcher: mango.ManualUpdateWatcher[
mango.PlacedOrdersContainer] = mango.ManualUpdateWatcher(placed_orders_container)
inventory_watcher: mango.ManualUpdateWatcher[mango.Inventory] = mango.ManualUpdateWatcher(inventory)
bids_watcher: mango.ManualUpdateWatcher[typing.Sequence[mango.Order]] = mango.ManualUpdateWatcher(bids)
asks_watcher: mango.ManualUpdateWatcher[typing.Sequence[mango.Order]] = mango.ManualUpdateWatcher(asks)
orderbook_watcher: mango.ManualUpdateWatcher[mango.OrderBook] = mango.ManualUpdateWatcher(orderbook)
return mango.ModelState(order_owner, market, group_watcher,
account_watcher, price_watcher, placed_orders_container_watcher,
inventory_watcher, bids_watcher, asks_watcher)
inventory_watcher, orderbook_watcher)

View File

@ -25,7 +25,8 @@ asks: typing.Sequence[mango.Order] = [
fake_order(price=Decimal(86), quantity=Decimal(3), side=mango.Side.SELL),
fake_order(price=Decimal(87), quantity=Decimal(7), side=mango.Side.SELL)
]
model_state = fake_model_state(bids=bids, asks=asks)
orderbook: mango.OrderBook = mango.OrderBook("TEST", bids, asks)
model_state = fake_model_state(orderbook=orderbook)
def test_from_args():
@ -73,7 +74,8 @@ def test_accumulation_ignores_own_orders_updated():
fake_order(price=Decimal(86), quantity=Decimal(3), side=mango.Side.SELL),
fake_order(price=Decimal(87), quantity=Decimal(7), side=mango.Side.SELL)
]
model_state = fake_model_state(order_owner=order_owner, bids=bids, asks=asks)
orderbook: mango.OrderBook = mango.OrderBook("TEST", bids, asks)
model_state = fake_model_state(order_owner=order_owner, orderbook=orderbook)
context = fake_context()
buy: mango.Order = fake_order(price=Decimal(78), quantity=Decimal(6), side=mango.Side.BUY)
sell: mango.Order = fake_order(price=Decimal(82), quantity=Decimal(6), side=mango.Side.SELL)

View File

@ -13,7 +13,9 @@ top_bid: mango.Order = fake_order(price=Decimal(90), side=mango.Side.BUY, order_
# The top ask is the lowest price someone is willing to pay to SELL
top_ask: mango.Order = fake_order(price=Decimal(110), side=mango.Side.SELL, order_type=mango.OrderType.POST_ONLY)
model_state = fake_model_state(market=fake_loaded_market(), bids=[top_bid], asks=[top_ask])
orderbook: mango.OrderBook = mango.OrderBook("TEST", [top_bid], [top_ask])
model_state = fake_model_state(market=fake_loaded_market(), orderbook=orderbook)
def test_from_args():
@ -77,7 +79,8 @@ def test_bid_too_high_no_bid_results_in_new_bid():
order: mango.Order = fake_order(price=Decimal(120), side=mango.Side.BUY, order_type=mango.OrderType.POST_ONLY)
actual: PreventPostOnlyCrossingBookElement = PreventPostOnlyCrossingBookElement()
model_state = fake_model_state(market=fake_loaded_market(), bids=[], asks=[top_ask])
orderbook: mango.OrderBook = mango.OrderBook("TEST", [], [top_ask])
model_state = fake_model_state(market=fake_loaded_market(), orderbook=orderbook)
result = actual.process(context, model_state, [order])
@ -89,7 +92,8 @@ def test_ask_too_low_no_ask_results_in_new_ask():
order: mango.Order = fake_order(price=Decimal(80), side=mango.Side.SELL, order_type=mango.OrderType.POST_ONLY)
actual: PreventPostOnlyCrossingBookElement = PreventPostOnlyCrossingBookElement()
model_state = fake_model_state(market=fake_loaded_market(), bids=[top_bid], asks=[])
orderbook: mango.OrderBook = mango.OrderBook("TEST", [top_bid], [])
model_state = fake_model_state(market=fake_loaded_market(), orderbook=orderbook)
result = actual.process(context, model_state, [order])
assert result[0].price == 91
@ -100,7 +104,8 @@ def test_ask_no_orderbook_results_in_no_change():
order: mango.Order = fake_order(price=Decimal(120), side=mango.Side.SELL, order_type=mango.OrderType.POST_ONLY)
actual: PreventPostOnlyCrossingBookElement = PreventPostOnlyCrossingBookElement()
model_state = fake_model_state(market=fake_loaded_market(), bids=[], asks=[])
orderbook: mango.OrderBook = mango.OrderBook("TEST", [], [])
model_state = fake_model_state(market=fake_loaded_market(), orderbook=orderbook)
result = actual.process(context, model_state, [order])
assert result == [order]
@ -111,7 +116,8 @@ def test_bid_no_orderbook_results_in_no_change():
order: mango.Order = fake_order(price=Decimal(80), side=mango.Side.BUY, order_type=mango.OrderType.POST_ONLY)
actual: PreventPostOnlyCrossingBookElement = PreventPostOnlyCrossingBookElement()
model_state = fake_model_state(market=fake_loaded_market(), bids=[], asks=[])
orderbook: mango.OrderBook = mango.OrderBook("TEST", [], [])
model_state = fake_model_state(market=fake_loaded_market(), orderbook=orderbook)
result = actual.process(context, model_state, [order])
assert result == [order]