Added OrderBook instead of passing around bids and asks. Added it to ModelState too, and cleaned up usage.
This commit is contained in:
parent
c2615218e3
commit
56599a1037
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"])
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}' »"""
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 []
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}] »"""
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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] = []
|
||||
|
|
|
@ -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()}
|
||||
|
|
|
@ -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] = []
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
|
Loading…
Reference in New Issue