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

View File

@ -45,7 +45,7 @@ from .observables import DisposePropagator, DisposeWrapper, NullObserverSubscrib
from .openorders import OpenOrders from .openorders import OpenOrders
from .oracle import OracleSource, Price, Oracle, OracleProvider, SupportedOracleFeature from .oracle import OracleSource, Price, Oracle, OracleProvider, SupportedOracleFeature
from .orderbookside import OrderBookSideType, PerpOrderBookSide from .orderbookside import OrderBookSideType, PerpOrderBookSide
from .orders import Order, OrderType, Side from .orders import Order, OrderType, Side, OrderBook
from .ownedtokenvalue import OwnedTokenValue from .ownedtokenvalue import OwnedTokenValue
from .oraclefactory import create_oracle_provider from .oraclefactory import create_oracle_provider
from .parse_account_info_to_orders import parse_account_info_to_orders 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 .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 .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 .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 .websocketsubscription import WebSocketSubscription, WebSocketProgramSubscription, WebSocketAccountSubscription, WebSocketLogSubscription, WebSocketSubscriptionManager, IndividualWebSocketSubscriptionManager, SharedWebSocketSubscriptionManager
from .layouts import layouts from .layouts import layouts

View File

@ -200,6 +200,28 @@ class Account(AddressableAccount):
raise Exception(f"Account account not found at address '{address}'") raise Exception(f"Account account not found at address '{address}'")
return Account.parse(account_info, group) 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 @staticmethod
def load_all_for_owner(context: Context, owner: PublicKey, group: Group) -> typing.Sequence["Account"]: 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. # 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 = [] accounts = []
for account_data in results: for account_data in results:
address = PublicKey(account_data["pubkey"]) address = PublicKey(account_data["pubkey"])

View File

@ -13,15 +13,15 @@
# [Github](https://github.com/blockworks-foundation) # [Github](https://github.com/blockworks-foundation)
# [Email](mailto:hello@blockworks.foundation) # [Email](mailto:hello@blockworks.foundation)
import typing import typing
from solana.publickey import PublicKey from solana.publickey import PublicKey
from .accountinfo import AccountInfo
from .context import Context from .context import Context
from .lotsizeconverter import LotSizeConverter from .lotsizeconverter import LotSizeConverter
from .market import Market, InventorySource from .market import Market, InventorySource
from .orders import Order from .orders import Order, OrderBook
from .token import Token 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): 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) super().__init__(program_address, address, inventory_source, base, quote, lot_size_converter)
def orders(self, context: Context) -> typing.Sequence[Order]: @property
raise NotImplementedError("LoadedMarket.orders() is not implemented on the base type.") 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, def from_values(self, order_owner: PublicKey, market: mango.Market, group: mango.Group, account: mango.Account,
price: mango.Price, placed_orders_container: mango.PlacedOrdersContainer, price: mango.Price, placed_orders_container: mango.PlacedOrdersContainer,
inventory: mango.Inventory, bids: typing.Sequence[mango.Order], inventory: mango.Inventory, orderbook: mango.OrderBook) -> ModelState:
asks: typing.Sequence[mango.Order]) -> ModelState:
group_watcher: mango.ManualUpdateWatcher[mango.Group] = mango.ManualUpdateWatcher(group) group_watcher: mango.ManualUpdateWatcher[mango.Group] = mango.ManualUpdateWatcher(group)
account_watcher: mango.ManualUpdateWatcher[mango.Account] = mango.ManualUpdateWatcher(account) account_watcher: mango.ManualUpdateWatcher[mango.Account] = mango.ManualUpdateWatcher(account)
price_watcher: mango.ManualUpdateWatcher[mango.Price] = mango.ManualUpdateWatcher(price) price_watcher: mango.ManualUpdateWatcher[mango.Price] = mango.ManualUpdateWatcher(price)
placed_orders_container_watcher: mango.ManualUpdateWatcher[ placed_orders_container_watcher: mango.ManualUpdateWatcher[
mango.PlacedOrdersContainer] = mango.ManualUpdateWatcher(placed_orders_container) mango.PlacedOrdersContainer] = mango.ManualUpdateWatcher(placed_orders_container)
inventory_watcher: mango.ManualUpdateWatcher[mango.Inventory] = mango.ManualUpdateWatcher(inventory) inventory_watcher: mango.ManualUpdateWatcher[mango.Inventory] = mango.ManualUpdateWatcher(inventory)
bids_watcher: mango.ManualUpdateWatcher[typing.Sequence[mango.Order]] = mango.ManualUpdateWatcher(bids) orderbook_watcher: mango.ManualUpdateWatcher[mango.OrderBook] = mango.ManualUpdateWatcher(orderbook)
asks_watcher: mango.ManualUpdateWatcher[typing.Sequence[mango.Order]] = mango.ManualUpdateWatcher(asks)
return ModelState(order_owner, market, group_watcher, account_watcher, price_watcher, 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: def __str__(self) -> str:
return "« 𝙿𝚘𝚕𝚕𝚒𝚗𝚐𝙼𝚘𝚍𝚎𝚕𝚂𝚝𝚊𝚝𝚎𝙱𝚞𝚒𝚕𝚍𝚎𝚛 »" return "« 𝙿𝚘𝚕𝚕𝚒𝚗𝚐𝙼𝚘𝚍𝚎𝚕𝚂𝚝𝚊𝚝𝚎𝙱𝚞𝚒𝚕𝚍𝚎𝚛 »"
@ -138,8 +136,8 @@ class SerumPollingModelStateBuilder(PollingModelStateBuilder):
self.open_orders_address, self.open_orders_address,
self.base_inventory_token_account.address, self.base_inventory_token_account.address,
self.quote_inventory_token_account.address, self.quote_inventory_token_account.address,
self.market.underlying_serum_market.state.bids(), self.market.bids_address,
self.market.underlying_serum_market.state.asks() self.market.asks_address
] ]
account_infos: typing.Sequence[mango.AccountInfo] = mango.AccountInfo.load_multiple(context, addresses) account_infos: typing.Sequence[mango.AccountInfo] = mango.AccountInfo.load_multiple(context, addresses)
group: mango.Group = mango.Group.parse(context, account_infos[0]) group: mango.Group = mango.Group.parse(context, account_infos[0])
@ -156,11 +154,7 @@ class SerumPollingModelStateBuilder(PollingModelStateBuilder):
quote_inventory_token_account = mango.TokenAccount.parse( quote_inventory_token_account = mango.TokenAccount.parse(
account_infos[4], self.quote_inventory_token_account.value.token) account_infos[4], self.quote_inventory_token_account.value.token)
# Both these will have top-of-book at index 0. orderbook: mango.OrderBook = self.market.parse_account_infos_to_orderbook(account_infos[5], account_infos[6])
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)
price: mango.Price = self.oracle.fetch_price(context) price: mango.Price = self.oracle.fetch_price(context)
@ -173,7 +167,7 @@ class SerumPollingModelStateBuilder(PollingModelStateBuilder):
base_inventory_token_account.value, base_inventory_token_account.value,
quote_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: def __str__(self) -> str:
return f"""« 𝚂𝚎𝚛𝚞𝚖𝙿𝚘𝚕𝚕𝚒𝚗𝚐𝙼𝚘𝚍𝚎𝚕𝚂𝚝𝚊𝚝𝚎𝙱𝚞𝚒𝚕𝚍𝚎𝚛 for market '{self.market.symbol}' »""" return f"""« 𝚂𝚎𝚛𝚞𝚖𝙿𝚘𝚕𝚕𝚒𝚗𝚐𝙼𝚘𝚍𝚎𝚕𝚂𝚝𝚊𝚝𝚎𝙱𝚞𝚒𝚕𝚍𝚎𝚛 for market '{self.market.symbol}' »"""
@ -212,8 +206,8 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder):
self.group_address, self.group_address,
self.cache_address, self.cache_address,
self.account_address, self.account_address,
self.market.underlying_serum_market.state.bids(), self.market.bids_address,
self.market.underlying_serum_market.state.asks(), self.market.asks_address,
*self.all_open_orders_addresses *self.all_open_orders_addresses
] ]
account_infos: typing.Sequence[mango.AccountInfo] = mango.AccountInfo.load_multiple(context, addresses) account_infos: typing.Sequence[mango.AccountInfo] = mango.AccountInfo.load_multiple(context, addresses)
@ -254,15 +248,11 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder):
base_value, base_value,
quote_value) quote_value)
# Both these will have top-of-book at index 0. orderbook: mango.OrderBook = self.market.parse_account_infos_to_orderbook(account_infos[3], account_infos[4])
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)
price: mango.Price = self.oracle.fetch_price(context) 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: def __str__(self) -> str:
return f"""« 𝚂𝚙𝚘𝚝𝙿𝚘𝚕𝚕𝚒𝚗𝚐𝙼𝚘𝚍𝚎𝚕𝚂𝚝𝚊𝚝𝚎𝙱𝚞𝚒𝚕𝚍𝚎𝚛 for market '{self.market.symbol}' »""" return f"""« 𝚂𝚙𝚘𝚝𝙿𝚘𝚕𝚕𝚒𝚗𝚐𝙼𝚘𝚍𝚎𝚕𝚂𝚝𝚊𝚝𝚎𝙱𝚞𝚒𝚕𝚍𝚎𝚛 for market '{self.market.symbol}' »"""
@ -322,15 +312,11 @@ class PerpPollingModelStateBuilder(PollingModelStateBuilder):
base_token_value, base_token_value,
quote_token_value) quote_token_value)
# Both these will have top-of-book at index 0. orderbook: mango.OrderBook = self.market.parse_account_infos_to_orderbook(account_infos[3], account_infos[4])
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)
price: mango.Price = self.oracle.fetch_price(context) 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: def __str__(self) -> str:
return f"""« 𝙿𝚎𝚛𝚙𝙿𝚘𝚕𝚕𝚒𝚗𝚐𝙼𝚘𝚍𝚎𝚕𝚂𝚝𝚊𝚝𝚎𝙱𝚞𝚒𝚕𝚍𝚎𝚛 for market '{self.market.symbol}' »""" 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) context, websocket_manager, health_check, disposer, wallet, market, price_watcher)
latest_open_orders_observer: mango.Watcher[mango.PlacedOrdersContainer] = mango.build_serum_open_orders_watcher( latest_open_orders_observer: mango.Watcher[mango.PlacedOrdersContainer] = mango.build_serum_open_orders_watcher(
context, websocket_manager, health_check, market, wallet) context, websocket_manager, health_check, market, wallet)
latest_bids_watcher: mango.Watcher[typing.Sequence[mango.Order]] = mango.build_serum_orderbook_side_watcher( latest_orderbook_watcher = mango.build_orderbook_watcher(
context, websocket_manager, health_check, market.underlying_serum_market, mango.OrderBookSideType.BIDS) context, websocket_manager, health_check, market)
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)
elif isinstance(market, mango.SpotMarket): elif isinstance(market, mango.SpotMarket):
market_index: int = group.find_spot_market_index(market.address) market_index: int = group.find_spot_market_index(market.address)
order_owner = account.spot_open_orders[market_index] or SYSTEM_PROGRAM_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( inventory_watcher = mango.SpotInventoryAccountWatcher(
market, latest_account_observer, latest_group_observer, all_open_orders_watchers, cache_watcher) market, latest_account_observer, latest_group_observer, all_open_orders_watchers, cache_watcher)
latest_bids_watcher = mango.build_serum_orderbook_side_watcher( latest_orderbook_watcher = mango.build_orderbook_watcher(
context, websocket_manager, health_check, market.underlying_serum_market, mango.OrderBookSideType.BIDS) context, websocket_manager, health_check, market)
latest_asks_watcher = mango.build_serum_orderbook_side_watcher(
context, websocket_manager, health_check, market.underlying_serum_market, mango.OrderBookSideType.ASKS)
elif isinstance(market, mango.PerpMarket): elif isinstance(market, mango.PerpMarket):
order_owner = account.address order_owner = account.address
cache = mango.Cache.load(context, group.cache) 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) market, latest_account_observer, latest_group_observer, cache_watcher, group)
latest_open_orders_observer = mango.build_perp_open_orders_watcher( latest_open_orders_observer = mango.build_perp_open_orders_watcher(
context, websocket_manager, health_check, market, account, group, account_subscription) context, websocket_manager, health_check, market, account, group, account_subscription)
latest_bids_watcher = mango.build_perp_orderbook_side_watcher( latest_orderbook_watcher = mango.build_orderbook_watcher(
context, websocket_manager, health_check, market, mango.OrderBookSideType.BIDS) context, websocket_manager, health_check, market)
latest_asks_watcher = mango.build_perp_orderbook_side_watcher(
context, websocket_manager, health_check, market, mango.OrderBookSideType.ASKS)
else: else:
raise Exception(f"Could not determine type of market {market.symbol}") raise Exception(f"Could not determine type of market {market.symbol}")
model_state = ModelState(order_owner, market, latest_group_observer, latest_account_observer, model_state = ModelState(order_owner, market, latest_group_observer, latest_account_observer,
latest_price_observer, latest_open_orders_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) return WebsocketModelStateBuilder(model_state)

View File

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

View File

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

View File

@ -27,7 +27,7 @@ from ...loadedmarket import LoadedMarket
from ...market import Market from ...market import Market
from ...observables import observable_pipeline_error_reporter from ...observables import observable_pipeline_error_reporter
from ...oracle import Oracle, OracleProvider, OracleSource, Price, SupportedOracleFeature from ...oracle import Oracle, OracleProvider, OracleSource, Price, SupportedOracleFeature
from ...orders import Order, Side from ...orders import OrderBook
# # 🥭 Market # # 🥭 Market
@ -59,10 +59,18 @@ class MarketOracle(Oracle):
self.source: OracleSource = OracleSource("Market", name, features, market) self.source: OracleSource = OracleSource("Market", name, features, market)
def fetch_price(self, context: Context) -> Price: def fetch_price(self, context: Context) -> Price:
orders: typing.Sequence[Order] = self.loaded_market.orders(context) orderbook: OrderBook = self.loaded_market.fetch_orderbook(context)
top_bid = max([order.price for order in orders if order.side == Side.BUY]) if orderbook.top_bid is None:
top_ask = min([order.price for order in orders if order.side == Side.SELL]) raise Exception(f"[{self.source}] Cannot determine complete price data - no top bid")
mid_price = (top_bid + top_ask) / 2 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) 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) return PerpOrderBookSide(account_info, version, meta_data, perp_market_details, bump_index, free_list_len, free_list_head, root_node, leaf_count, nodes)
@staticmethod @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 data = account_info.data
if len(data) != layouts.ORDERBOOK_SIDE.sizeof(): if len(data) != layouts.ORDERBOOK_SIDE.sizeof():
raise Exception( raise Exception(
@ -94,7 +94,7 @@ class PerpOrderBookSide(AddressableAccount):
account_info = AccountInfo.load(context, address) account_info = AccountInfo.load(context, address)
if account_info is None: if account_info is None:
raise Exception(f"PerpOrderBookSide account not found at address '{address}'") 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]: def orders(self) -> typing.Sequence[Order]:
if self.leaf_count == 0: if self.leaf_count == 0:

View File

@ -189,3 +189,76 @@ class Order(typing.NamedTuple):
def __repr__(self) -> str: def __repr__(self) -> str:
return f"{self}" 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: def group(self) -> Group:
return self.underlying_perp_market.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]: def unprocessed_events(self, context: Context) -> typing.Sequence[PerpEvent]:
event_queue: PerpEventQueue = PerpEventQueue.load( event_queue: PerpEventQueue = PerpEventQueue.load(
context, self.underlying_perp_market.event_queue, self.lot_size_converter) 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]) distinct.sort(key=lambda address: address._key or [0])
return distinct 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: def observe_events(self, context: Context, interval: int = 30) -> DisposingSubject:
perp_event_queue: PerpEventQueue = PerpEventQueue.load( perp_event_queue: PerpEventQueue = PerpEventQueue.load(
context, self.underlying_perp_market.event_queue, self.lot_size_converter) 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 .constants import SYSTEM_PROGRAM_ADDRESS
from .context import Context from .context import Context
from .marketoperations import MarketOperations from .marketoperations import MarketOperations
from .orders import Order from .orders import Order, OrderBook
from .perpmarketinstructionbuilder import PerpMarketInstructionBuilder from .perpmarketinstructionbuilder import PerpMarketInstructionBuilder
from .perpmarket import PerpMarket from .perpmarket import PerpMarket
from .wallet import Wallet from .wallet import Wallet
@ -86,12 +86,12 @@ class PerpMarketOperations(MarketOperations):
def ensure_openorders(self) -> PublicKey: def ensure_openorders(self) -> PublicKey:
return SYSTEM_PROGRAM_ADDRESS return SYSTEM_PROGRAM_ADDRESS
def load_orders(self) -> typing.Sequence[Order]: def load_orderbook(self) -> OrderBook:
return self.perp_market.orders(self.context) return self.perp_market.fetch_orderbook(self.context)
def load_my_orders(self) -> typing.Sequence[Order]: def load_my_orders(self) -> typing.Sequence[Order]:
all_orders = self.perp_market.orders(self.context) orderbook: OrderBook = self.load_orderbook()
return list([o for o in all_orders if o.owner == self.account.address]) return list([o for o in [*orderbook.bids, *orderbook.asks] if o.owner == self.account.address])
def __str__(self) -> str: def __str__(self) -> str:
return f"""« 𝙿𝚎𝚛𝚙𝚜𝙾𝚛𝚍𝚎𝚛𝙿𝚕𝚊𝚌𝚎𝚛 [{self.market_name}] »""" return f"""« 𝙿𝚎𝚛𝚙𝚜𝙾𝚛𝚍𝚎𝚛𝙿𝚕𝚊𝚌𝚎𝚛 [{self.market_name}] »"""

View File

@ -13,7 +13,6 @@
# [Github](https://github.com/blockworks-foundation) # [Github](https://github.com/blockworks-foundation)
# [Email](mailto:hello@blockworks.foundation) # [Email](mailto:hello@blockworks.foundation)
import itertools
import typing import typing
from decimal import Decimal from decimal import Decimal
@ -44,19 +43,22 @@ class SerumMarket(LoadedMarket):
quote_lot_size: Decimal = Decimal(underlying_serum_market.state.quote_lot_size()) 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) 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]: 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.underlying_serum_market.state.event_queue())
return event_queue.unprocessed_events 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]: def find_openorders_address_for_owner(self, context: Context, owner: PublicKey) -> typing.Optional[PublicKey]:
all_open_orders = OpenOrders.load_for_market_and_owner( all_open_orders = OpenOrders.load_for_market_and_owner(
context, self.address, owner, context.serum_program_address, self.base.decimals, self.quote.decimals) 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 .constants import SYSTEM_PROGRAM_ADDRESS
from .context import Context from .context import Context
from .marketoperations import MarketOperations from .marketoperations import MarketOperations
from .orders import Order from .orders import Order, OrderBook
from .serummarket import SerumMarket from .serummarket import SerumMarket
from .serummarketinstructionbuilder import SerumMarketInstructionBuilder from .serummarketinstructionbuilder import SerumMarketInstructionBuilder
from .wallet import Wallet from .wallet import Wallet
@ -90,16 +90,16 @@ class SerumMarketOperations(MarketOperations):
return self.market_instruction_builder.open_orders_address return self.market_instruction_builder.open_orders_address
return self.create_openorders() return self.create_openorders()
def load_orders(self) -> typing.Sequence[Order]: def load_orderbook(self) -> OrderBook:
return self.serum_market.orders(self.context) return self.serum_market.fetch_orderbook(self.context)
def load_my_orders(self) -> typing.Sequence[Order]: def load_my_orders(self) -> typing.Sequence[Order]:
open_orders_address = self.market_instruction_builder.open_orders_address open_orders_address = self.market_instruction_builder.open_orders_address
if not open_orders_address: if not open_orders_address:
return [] return []
all_orders = self.serum_market.orders(self.context) orderbook: OrderBook = self.load_orderbook()
return list([o for o in all_orders if o.owner == open_orders_address]) 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: def _build_crank(self, limit: Decimal = Decimal(32)) -> CombinableInstructions:
open_orders_to_crank: typing.List[PublicKey] = [] open_orders_to_crank: typing.List[PublicKey] = []

View File

@ -13,7 +13,6 @@
# [Github](https://github.com/blockworks-foundation) # [Github](https://github.com/blockworks-foundation)
# [Email](mailto:hello@blockworks.foundation) # [Email](mailto:hello@blockworks.foundation)
import itertools
import typing import typing
from decimal import Decimal from decimal import Decimal
@ -45,19 +44,22 @@ class SpotMarket(LoadedMarket):
quote_lot_size: Decimal = Decimal(underlying_serum_market.state.quote_lot_size()) 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) 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]: 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.underlying_serum_market.state.event_queue())
return event_queue.unprocessed_events 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: def __str__(self) -> str:
return f"""« 𝚂𝚙𝚘𝚝𝙼𝚊𝚛𝚔𝚎𝚝 {self.symbol} {self.address} [{self.program_address}] return f"""« 𝚂𝚙𝚘𝚝𝙼𝚊𝚛𝚔𝚎𝚝 {self.symbol} {self.address} [{self.program_address}]
Event Queue: {self.underlying_serum_market.state.event_queue()} 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 .context import Context
from .group import Group from .group import Group
from .marketoperations import MarketOperations from .marketoperations import MarketOperations
from .orders import Order from .orders import Order, OrderBook
from .spotmarket import SpotMarket from .spotmarket import SpotMarket
from .spotmarketinstructionbuilder import SpotMarketInstructionBuilder from .spotmarketinstructionbuilder import SpotMarketInstructionBuilder
from .wallet import Wallet from .wallet import Wallet
@ -102,15 +102,15 @@ class SpotMarketOperations(MarketOperations):
return existing return existing
return self.create_openorders() return self.create_openorders()
def load_orders(self) -> typing.Sequence[Order]: def load_orderbook(self) -> OrderBook:
return self.spot_market.orders(self.context) return self.spot_market.fetch_orderbook(self.context)
def load_my_orders(self) -> typing.Sequence[Order]: def load_my_orders(self) -> typing.Sequence[Order]:
if not self.open_orders_address: if not self.open_orders_address:
return [] return []
all_orders = self.spot_market.orders(self.context) orderbook: OrderBook = self.load_orderbook()
return list([o for o in all_orders if o.owner == self.open_orders_address]) 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: def _build_crank(self, limit: Decimal = Decimal(32), add_self: bool = False) -> CombinableInstructions:
open_orders_to_crank: typing.List[PublicKey] = [] open_orders_to_crank: typing.List[PublicKey] = []

View File

@ -139,9 +139,11 @@ class ImmediateTradeExecutor(TradeExecutor):
def buy(self, symbol: str, quantity: Decimal) -> Order: def buy(self, symbol: str, quantity: Decimal) -> Order:
market_operations: MarketOperations = self._build_market_operations(symbol) 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 increase_factor = Decimal(1) + self.price_adjustment_factor
price = top_ask * increase_factor price = top_ask * increase_factor
@ -152,9 +154,11 @@ class ImmediateTradeExecutor(TradeExecutor):
def sell(self, symbol: str, quantity: Decimal) -> Order: def sell(self, symbol: str, quantity: Decimal) -> Order:
market_operations: MarketOperations = self._build_market_operations(symbol) 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 decrease_factor = Decimal(1) - self.price_adjustment_factor
price = top_bid * decrease_factor price = top_bid * decrease_factor

View File

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

View File

@ -144,11 +144,11 @@ def fake_inventory():
def fake_bids(): def fake_bids():
return None return []
def fake_asks(): def fake_asks():
return None return []
def fake_model_state(order_owner: typing.Optional[PublicKey] = None, 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, price: typing.Optional[mango.Price] = None,
placed_orders_container: typing.Optional[mango.PlacedOrdersContainer] = None, placed_orders_container: typing.Optional[mango.PlacedOrdersContainer] = None,
inventory: typing.Optional[mango.Inventory] = None, inventory: typing.Optional[mango.Inventory] = None,
bids: typing.Optional[typing.Sequence[mango.Order]] = None, orderbook: typing.Optional[mango.OrderBook] = None) -> mango.ModelState:
asks: typing.Optional[typing.Sequence[mango.Order]] = None) -> mango.ModelState:
order_owner = order_owner or fake_seeded_public_key("order owner") order_owner = order_owner or fake_seeded_public_key("order owner")
market = market or fake_loaded_market() market = market or fake_loaded_market()
group = group or fake_group() group = group or fake_group()
@ -167,17 +166,15 @@ def fake_model_state(order_owner: typing.Optional[PublicKey] = None,
price = price or fake_price() price = price or fake_price()
placed_orders_container = placed_orders_container or fake_placed_orders_container() placed_orders_container = placed_orders_container or fake_placed_orders_container()
inventory = inventory or fake_inventory() inventory = inventory or fake_inventory()
bids = bids or fake_bids() orderbook = orderbook or mango.OrderBook("FAKE", fake_bids(), fake_asks())
asks = asks or fake_asks()
group_watcher: mango.ManualUpdateWatcher[mango.Group] = mango.ManualUpdateWatcher(group) group_watcher: mango.ManualUpdateWatcher[mango.Group] = mango.ManualUpdateWatcher(group)
account_watcher: mango.ManualUpdateWatcher[mango.Account] = mango.ManualUpdateWatcher(account) account_watcher: mango.ManualUpdateWatcher[mango.Account] = mango.ManualUpdateWatcher(account)
price_watcher: mango.ManualUpdateWatcher[mango.Price] = mango.ManualUpdateWatcher(price) price_watcher: mango.ManualUpdateWatcher[mango.Price] = mango.ManualUpdateWatcher(price)
placed_orders_container_watcher: mango.ManualUpdateWatcher[ placed_orders_container_watcher: mango.ManualUpdateWatcher[
mango.PlacedOrdersContainer] = mango.ManualUpdateWatcher(placed_orders_container) mango.PlacedOrdersContainer] = mango.ManualUpdateWatcher(placed_orders_container)
inventory_watcher: mango.ManualUpdateWatcher[mango.Inventory] = mango.ManualUpdateWatcher(inventory) inventory_watcher: mango.ManualUpdateWatcher[mango.Inventory] = mango.ManualUpdateWatcher(inventory)
bids_watcher: mango.ManualUpdateWatcher[typing.Sequence[mango.Order]] = mango.ManualUpdateWatcher(bids) orderbook_watcher: mango.ManualUpdateWatcher[mango.OrderBook] = mango.ManualUpdateWatcher(orderbook)
asks_watcher: mango.ManualUpdateWatcher[typing.Sequence[mango.Order]] = mango.ManualUpdateWatcher(asks)
return mango.ModelState(order_owner, market, group_watcher, return mango.ModelState(order_owner, market, group_watcher,
account_watcher, price_watcher, placed_orders_container_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(86), quantity=Decimal(3), side=mango.Side.SELL),
fake_order(price=Decimal(87), quantity=Decimal(7), 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(): 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(86), quantity=Decimal(3), side=mango.Side.SELL),
fake_order(price=Decimal(87), quantity=Decimal(7), 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() context = fake_context()
buy: mango.Order = fake_order(price=Decimal(78), quantity=Decimal(6), side=mango.Side.BUY) 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) 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 # 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) 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(): 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) order: mango.Order = fake_order(price=Decimal(120), side=mango.Side.BUY, order_type=mango.OrderType.POST_ONLY)
actual: PreventPostOnlyCrossingBookElement = PreventPostOnlyCrossingBookElement() 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]) 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) order: mango.Order = fake_order(price=Decimal(80), side=mango.Side.SELL, order_type=mango.OrderType.POST_ONLY)
actual: PreventPostOnlyCrossingBookElement = PreventPostOnlyCrossingBookElement() 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]) result = actual.process(context, model_state, [order])
assert result[0].price == 91 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) order: mango.Order = fake_order(price=Decimal(120), side=mango.Side.SELL, order_type=mango.OrderType.POST_ONLY)
actual: PreventPostOnlyCrossingBookElement = PreventPostOnlyCrossingBookElement() 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]) result = actual.process(context, model_state, [order])
assert result == [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) order: mango.Order = fake_order(price=Decimal(80), side=mango.Side.BUY, order_type=mango.OrderType.POST_ONLY)
actual: PreventPostOnlyCrossingBookElement = PreventPostOnlyCrossingBookElement() 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]) result = actual.process(context, model_state, [order])
assert result == [order] assert result == [order]