ModelState now tracks current orders on the orderbook directly, rather than using OpenOrders.

This commit is contained in:
Geoff Taylor 2021-09-02 13:11:35 +01:00
parent 39c591fc6f
commit 1d4e24ea38
8 changed files with 48 additions and 90 deletions

View File

@ -4,6 +4,5 @@ from .modelstate import ModelState
from .modelstatebuilder import ModelStateBuilder, WebsocketModelStateBuilder, PollingModelStateBuilder, SerumPollingModelStateBuilder, SpotPollingModelStateBuilder, PerpPollingModelStateBuilder
from .modelstatebuilderfactory import ModelUpdateMode, model_state_builder_factory
from .orderreconciler import OrderReconciler, NullOrderReconciler
from .ordertracker import OrderTracker
from .reconciledorders import ReconciledOrders
from .toleranceorderreconciler import ToleranceOrderReconciler

View File

@ -25,7 +25,6 @@ from decimal import Decimal
from .modelstate import ModelState
from ..observables import EventSource
from .orderreconciler import OrderReconciler
from .ordertracker import OrderTracker
from .orderchain.chain import Chain
@ -46,7 +45,6 @@ class MarketMaker:
self.order_reconciler: OrderReconciler = order_reconciler
self.redeem_threshold: typing.Optional[Decimal] = redeem_threshold
self.order_tracker: OrderTracker = OrderTracker()
self.pulse_complete: EventSource[datetime] = EventSource[datetime]()
self.pulse_error: EventSource[Exception] = EventSource[Exception]()
@ -69,7 +67,7 @@ class MarketMaker:
self.logger.info(f"[{context.name}] Market-maker not quoting - model_state.not_quoting is set.")
return
existing_orders = self.order_tracker.existing_orders(model_state)
existing_orders = model_state.current_orders()
reconciled = self.order_reconciler.reconcile(model_state, existing_orders, desired_orders)
cancellations = mango.CombinableInstructions.empty()
@ -82,7 +80,6 @@ class MarketMaker:
for to_place in reconciled.to_place:
desired_client_id: int = context.random_client_id()
to_place_with_client_id = to_place.with_client_id(desired_client_id)
self.order_tracker.track(to_place_with_client_id)
self.logger.info(f"Placing {self.market.symbol} {to_place_with_client_id}")
place_order = self.market_instruction_builder.build_place_order_instructions(to_place_with_client_id)

View File

@ -20,13 +20,17 @@ import typing
from decimal import Decimal
from solana.publickey import PublicKey
# # 🥭 ModelState class
#
# Provides simple access to the latest state of market and account data.
#
class ModelState:
def __init__(self, market: mango.Market,
def __init__(self,
order_owner: PublicKey,
market: mango.Market,
group_watcher: mango.Watcher[mango.Group],
account_watcher: mango.Watcher[mango.Account],
price_watcher: mango.Watcher[mango.Price],
@ -36,6 +40,7 @@ class ModelState:
asks: mango.Watcher[typing.Sequence[mango.Order]]
):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.order_owner: PublicKey = order_owner
self.market: mango.Market = market
self.group_watcher: mango.Watcher[mango.Group] = group_watcher
self.account_watcher: mango.Watcher[mango.Account] = account_watcher
@ -103,9 +108,9 @@ class ModelState:
else:
return top_ask.price - top_bid.price
@property
def existing_orders(self) -> typing.Sequence[mango.PlacedOrder]:
return self.placed_orders_container_watcher.latest.placed_orders
def current_orders(self) -> typing.Sequence[mango.Order]:
all_orders = [*self.bids_watcher.latest, *self.asks_watcher.latest]
return list([o for o in all_orders if o.owner == self.order_owner])
def __str__(self) -> str:
return f"""« 𝙼𝚘𝚍𝚎𝚕𝚂𝚝𝚊𝚝𝚎 for market '{self.market.symbol}'

View File

@ -80,9 +80,10 @@ class PollingModelStateBuilder(ModelStateBuilder):
def poll(self, context: mango.Context) -> ModelState:
raise NotImplementedError("PollingModelStateBuilder.poll() is not implemented on the base type.")
def from_values(self, 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:
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:
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)
@ -92,7 +93,7 @@ class PollingModelStateBuilder(ModelStateBuilder):
bids_watcher: mango.ManualUpdateWatcher[typing.Sequence[mango.Order]] = mango.ManualUpdateWatcher(bids)
asks_watcher: mango.ManualUpdateWatcher[typing.Sequence[mango.Order]] = mango.ManualUpdateWatcher(asks)
return ModelState(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)
def __str__(self) -> str:
@ -105,6 +106,7 @@ class PollingModelStateBuilder(ModelStateBuilder):
#
class SerumPollingModelStateBuilder(PollingModelStateBuilder):
def __init__(self,
order_owner: PublicKey,
market: mango.SerumMarket,
oracle: mango.Oracle,
group_address: PublicKey,
@ -114,6 +116,7 @@ class SerumPollingModelStateBuilder(PollingModelStateBuilder):
quote_inventory_token_account: mango.TokenAccount,
):
super().__init__()
self.order_owner: PublicKey = order_owner
self.market: mango.SerumMarket = market
self.oracle: mango.Oracle = oracle
@ -164,7 +167,7 @@ class SerumPollingModelStateBuilder(PollingModelStateBuilder):
base_inventory_token_account.value,
quote_inventory_token_account.value)
return self.from_values(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, bids, asks)
def __str__(self) -> str:
return f"""« 𝚂𝚎𝚛𝚞𝚖𝙿𝚘𝚕𝚕𝚒𝚗𝚐𝙼𝚘𝚍𝚎𝚕𝚂𝚝𝚊𝚝𝚎𝙱𝚞𝚒𝚕𝚍𝚎𝚛 for market '{self.market.symbol}' »"""
@ -176,6 +179,7 @@ class SerumPollingModelStateBuilder(PollingModelStateBuilder):
#
class SpotPollingModelStateBuilder(PollingModelStateBuilder):
def __init__(self,
order_owner: PublicKey,
market: mango.SpotMarket,
oracle: mango.Oracle,
group_address: PublicKey,
@ -184,6 +188,7 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder):
open_orders_address: PublicKey
):
super().__init__()
self.order_owner: PublicKey = order_owner
self.market: mango.SpotMarket = market
self.oracle: mango.Oracle = oracle
@ -231,7 +236,7 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder):
price: mango.Price = self.oracle.fetch_price(context)
return self.from_values(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, bids, asks)
def __str__(self) -> str:
return f"""« 𝚂𝚙𝚘𝚝𝙿𝚘𝚕𝚕𝚒𝚗𝚐𝙼𝚘𝚍𝚎𝚕𝚂𝚝𝚊𝚝𝚎𝙱𝚞𝚒𝚕𝚍𝚎𝚛 for market '{self.market.symbol}' »"""
@ -243,6 +248,7 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder):
#
class PerpPollingModelStateBuilder(PollingModelStateBuilder):
def __init__(self,
order_owner: PublicKey,
market: mango.PerpMarket,
oracle: mango.Oracle,
group_address: PublicKey,
@ -250,6 +256,7 @@ class PerpPollingModelStateBuilder(PollingModelStateBuilder):
account_address: PublicKey
):
super().__init__()
self.order_owner: PublicKey = order_owner
self.market: mango.PerpMarket = market
self.oracle: mango.Oracle = oracle
@ -296,7 +303,7 @@ class PerpPollingModelStateBuilder(PollingModelStateBuilder):
price: mango.Price = self.oracle.fetch_price(context)
return self.from_values(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, bids.orders(), asks.orders())
def __str__(self) -> str:
return f"""« 𝙿𝚎𝚛𝚙𝙿𝚘𝚕𝚕𝚒𝚗𝚐𝙼𝚘𝚍𝚎𝚕𝚂𝚝𝚊𝚝𝚎𝙱𝚞𝚒𝚕𝚍𝚎𝚛 for market '{self.market.symbol}' »"""

View File

@ -14,6 +14,7 @@
# [Email](mailto:hello@blockworks.foundation)
import enum
from mango.constants import SYSTEM_PROGRAM_ADDRESS
import mango
import typing
@ -81,7 +82,7 @@ def _polling_serum_model_state_builder_factory(context: mango.Context, wallet: m
raise Exception(
f"Could not find serum openorders account owned by {wallet.address} for market {market.symbol}.")
return SerumPollingModelStateBuilder(
market, oracle, group.address, account.address, all_open_orders[0].address, base_account, quote_account)
all_open_orders[0].address, market, oracle, group.address, account.address, all_open_orders[0].address, base_account, quote_account)
def _polling_spot_model_state_builder_factory(group: mango.Group, account: mango.Account, market: mango.SpotMarket,
@ -92,12 +93,12 @@ def _polling_spot_model_state_builder_factory(group: mango.Group, account: mango
raise Exception(
f"Could not find spot openorders in account {account.address} for market {market.symbol}.")
return SpotPollingModelStateBuilder(
market, oracle, group.address, group.cache, account.address, open_orders_address)
open_orders_address, market, oracle, group.address, group.cache, account.address, open_orders_address)
def _polling_perp_model_state_builder_factory(group: mango.Group, account: mango.Account, market: mango.PerpMarket,
oracle: mango.Oracle) -> ModelStateBuilder:
return PerpPollingModelStateBuilder(market, oracle, group.address, group.cache, account.address)
return PerpPollingModelStateBuilder(account.address, market, oracle, group.address, group.cache, account.address)
def _websocket_model_state_builder_factory(context: mango.Context, disposer: mango.DisposePropagator,
@ -118,6 +119,8 @@ def _websocket_model_state_builder_factory(context: mango.Context, disposer: man
market = mango.ensure_market_loaded(context, market)
if isinstance(market, mango.SerumMarket):
order_owner: PublicKey = market.find_openorders_address_for_owner(
context, wallet.address) or SYSTEM_PROGRAM_ADDRESS
price_watcher: mango.Watcher[mango.Price] = mango.build_price_watcher(
context, websocket_manager, health_check, disposer, "serum", market)
inventory_watcher: mango.Watcher[mango.Inventory] = mango.build_serum_inventory_watcher(
@ -129,6 +132,8 @@ def _websocket_model_state_builder_factory(context: mango.Context, disposer: man
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):
market_index: int = group.find_spot_market_index(market.address)
order_owner = account.spot_open_orders[market_index] or SYSTEM_PROGRAM_ADDRESS
cache: mango.Cache = mango.Cache.load(context, group.cache)
cache_watcher: mango.Watcher[mango.Cache] = mango.build_cache_watcher(
context, websocket_manager, health_check, cache, group)
@ -140,6 +145,7 @@ def _websocket_model_state_builder_factory(context: mango.Context, disposer: man
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):
order_owner = account.address
cache = mango.Cache.load(context, group.cache)
cache_watcher = mango.build_cache_watcher(context, websocket_manager, health_check, cache, group)
inventory_watcher = mango.PerpInventoryAccountWatcher(market, latest_account_observer, cache_watcher, group)
@ -152,7 +158,7 @@ def _websocket_model_state_builder_factory(context: mango.Context, disposer: man
else:
raise Exception(f"Could not determine type of market {market.symbol}")
model_state = ModelState(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,
inventory_watcher, latest_bids_watcher, latest_asks_watcher)
return WebsocketModelStateBuilder(model_state)

View File

@ -1,67 +0,0 @@
# # ⚠ Warning
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# [🥭 Mango Markets](https://mango.markets/) support is available at:
# [Docs](https://docs.mango.markets/)
# [Discord](https://discord.gg/67jySBhxrg)
# [Twitter](https://twitter.com/mangomarkets)
# [Github](https://github.com/blockworks-foundation)
# [Email](mailto:hello@blockworks.foundation)
import logging
import mango
import typing
from collections import deque
from .modelstate import ModelState
# # 🥭 OrderTracker class
#
# Maintains a history of orders that were placed (or at least an attempt was made).
#
class OrderTracker:
def __init__(self, max_history: int = 20):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
self.tracked: typing.Deque[mango.Order] = deque(maxlen=max_history)
def track(self, order: mango.Order):
self.tracked += [order]
def existing_orders(self, model_state: ModelState) -> typing.Sequence[mango.Order]:
live_orders: typing.List[mango.Order] = []
for existing_order in model_state.existing_orders:
details = self._find_tracked(existing_order.client_id)
if details is None:
self.logger.warning(f"Could not find existing order with client ID {existing_order.client_id}")
# Return a stub order so that the Reconciler has the chance to cancel it.
stub = mango.Order.from_ids(existing_order.id, existing_order.client_id, existing_order.side)
live_orders += [stub]
else:
if details.id != existing_order.id:
self.tracked.remove(details)
details = details.with_id(existing_order.id)
self.tracked += [details]
live_orders += [details]
return live_orders
def _find_tracked(self, client_id_to_find: int) -> typing.Optional[mango.Order]:
for tracked in self.tracked:
if tracked.client_id == client_id_to_find:
return tracked
return None
def __str__(self) -> str:
return """« 𝙾𝚛𝚍𝚎𝚛𝚁𝚎𝚌𝚘𝚗𝚌𝚒𝚕𝚎𝚛 »"""
def __repr__(self) -> str:
return f"{self}"

View File

@ -25,6 +25,7 @@ from .accountinfo import AccountInfo
from .context import Context
from .lotsizeconverter import LotSizeConverter, RaisingLotSizeConverter
from .market import Market, InventorySource
from .openorders import OpenOrders
from .orders import Order
from .serumeventqueue import SerumEvent, SerumEventQueue
from .token import Token
@ -55,6 +56,13 @@ class SerumMarket(Market):
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)
if len(all_open_orders) == 0:
return None
return all_open_orders[0].address
def __str__(self) -> str:
return f"""« 𝚂𝚎𝚛𝚞𝚖𝙼𝚊𝚛𝚔𝚎𝚝 {self.symbol} {self.address} [{self.program_address}]
Event Queue: {self.underlying_serum_market.state.event_queue()}

View File

@ -142,7 +142,8 @@ def fake_asks():
return None
def fake_model_state(market: typing.Optional[mango.Market] = None,
def fake_model_state(order_owner: typing.Optional[PublicKey] = None,
market: typing.Optional[mango.Market] = None,
group: typing.Optional[mango.Group] = None,
account: typing.Optional[mango.Account] = None,
price: typing.Optional[mango.Price] = None,
@ -150,6 +151,7 @@ def fake_model_state(market: typing.Optional[mango.Market] = None,
inventory: typing.Optional[mango.Inventory] = None,
bids: typing.Optional[typing.Sequence[mango.Order]] = None,
asks: typing.Optional[typing.Sequence[mango.Order]] = None) -> mango.marketmaking.ModelState:
order_owner = order_owner or fake_seeded_public_key("order owner")
market = market or fake_loaded_market()
group = group or fake_group()
account = account or fake_account()
@ -167,5 +169,6 @@ def fake_model_state(market: typing.Optional[mango.Market] = None,
bids_watcher: mango.ManualUpdateWatcher[typing.Sequence[mango.Order]] = mango.ManualUpdateWatcher(bids)
asks_watcher: mango.ManualUpdateWatcher[typing.Sequence[mango.Order]] = mango.ManualUpdateWatcher(asks)
return mango.marketmaking.ModelState(market, group_watcher, account_watcher, price_watcher,
placed_orders_container_watcher, inventory_watcher, bids_watcher, asks_watcher)
return mango.marketmaking.ModelState(order_owner, market, group_watcher,
account_watcher, price_watcher, placed_orders_container_watcher,
inventory_watcher, bids_watcher, asks_watcher)