Updated SpotCollateralCalculator to take tokens in OpenOrders into account.

This commit is contained in:
Geoff Taylor 2021-09-09 14:10:34 +01:00
parent bf36c672c0
commit 8c42537572
5 changed files with 73 additions and 25 deletions

View File

@ -21,6 +21,7 @@ from decimal import Decimal
from .account import Account
from .cache import Cache, PriceCache
from .openorders import OpenOrders
from .spotmarketinfo import SpotMarketInfo
from .tokenvalue import TokenValue
@ -29,7 +30,7 @@ class CollateralCalculator(metaclass=abc.ABCMeta):
def __init__(self):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
def calculate(self, account: Account, cache: Cache) -> TokenValue:
def calculate(self, account: Account, all_open_orders: typing.Dict[str, OpenOrders], cache: Cache) -> TokenValue:
raise NotImplementedError("CollateralCalculator.calculate() is not implemented on the base type.")
@ -37,7 +38,7 @@ class SerumCollateralCalculator(CollateralCalculator):
def __init__(self):
super().__init__()
def calculate(self, account: Account, cache: Cache) -> TokenValue:
def calculate(self, account: Account, all_open_orders: typing.Dict[str, OpenOrders], cache: Cache) -> TokenValue:
raise NotImplementedError("SerumCollateralCalculator.calculate() is not implemented.")
@ -54,7 +55,7 @@ class SpotCollateralCalculator(CollateralCalculator):
# Also from Daffy, same thread, when I said there were two `init_asset_weights`, one for spot and one for perp (https://discord.com/channels/791995070613159966/807051268304273408/882030633940054056):
# yes I think we ignore perps
#
def calculate(self, account: Account, cache: Cache) -> TokenValue:
def calculate(self, account: Account, all_open_orders: typing.Dict[str, OpenOrders], cache: Cache) -> TokenValue:
# Quote token calculation:
# total_collateral = deposits[QUOTE_INDEX] * deposit_index - borrows[QUOTE_INDEX] * borrow_index
# Note: the `AccountBasketToken` in the `Account` already factors the deposit and borrow index.
@ -70,12 +71,18 @@ class SpotCollateralCalculator(CollateralCalculator):
raise Exception(
f"Could not read spot market of token {basket_token.token_info.token.symbol} at index {index} of cache at {cache.address}")
in_orders: Decimal = Decimal(0)
if basket_token.spot_open_orders is not None and str(basket_token.spot_open_orders) in all_open_orders:
open_orders: OpenOrders = all_open_orders[str(basket_token.spot_open_orders)]
in_orders = open_orders.quote_token_total + \
(open_orders.base_token_total * token_price.price * spot_market.init_asset_weight)
# Base token calculations:
# total_collateral += prices[i] * (init_asset_weights[i] * deposits[i] * deposit_index - init_liab_weights[i] * borrows[i] * borrow_index)
# Note: the `AccountBasketToken` in the `Account` already factors the deposit and borrow index.
weighted: Decimal = token_price.price * ((
weighted: Decimal = in_orders + (token_price.price * ((
basket_token.deposit.value * spot_market.init_asset_weight) - (
basket_token.borrow.value * spot_market.init_liab_weight))
basket_token.borrow.value * spot_market.init_liab_weight)))
total += weighted
return TokenValue(account.group.shared_quote_token.token, total)
@ -94,7 +101,7 @@ class PerpCollateralCalculator(CollateralCalculator):
# Also from Daffy, same thread, when I said there were two `init_asset_weights`, one for spot and one for perp (https://discord.com/channels/791995070613159966/807051268304273408/882030633940054056):
# yes I think we ignore perps
#
def calculate(self, account: Account, cache: Cache) -> TokenValue:
def calculate(self, account: Account, all_open_orders: typing.Dict[str, OpenOrders], cache: Cache) -> TokenValue:
# Quote token calculation:
# total_collateral = deposits[QUOTE_INDEX] * deposit_index - borrows[QUOTE_INDEX] * borrow_index
# Note: the `AccountBasketToken` in the `Account` already factors the deposit and borrow index.

View File

@ -15,6 +15,7 @@
import logging
import typing
from decimal import Decimal
@ -23,6 +24,7 @@ from .cache import Cache
from .collateralcalculator import CollateralCalculator, SpotCollateralCalculator, PerpCollateralCalculator
from .group import Group
from .market import InventorySource, Market
from .openorders import OpenOrders
from .perpmarket import PerpMarket
from .tokenvalue import TokenValue
from .watcher import Watcher
@ -56,8 +58,9 @@ class Inventory:
class SpotInventoryAccountWatcher:
def __init__(self, market: Market, account_watcher: Watcher[Account], cache_watcher: Watcher[Cache]):
def __init__(self, market: Market, account_watcher: Watcher[Account], all_open_orders_watchers: typing.Sequence[Watcher[OpenOrders]], cache_watcher: Watcher[Cache]):
self.account_watcher: Watcher[Account] = account_watcher
self.all_open_orders_watchers: typing.Sequence[Watcher[OpenOrders]] = all_open_orders_watchers
self.cache_watcher: Watcher[Cache] = cache_watcher
account: Account = account_watcher.latest
base_value = TokenValue.find_by_symbol(account.net_assets, market.base.symbol)
@ -75,7 +78,9 @@ class SpotInventoryAccountWatcher:
mngo = account.group.find_token_info_by_symbol("MNGO").token
mngo_accrued: TokenValue = TokenValue(mngo, Decimal(0))
available_collateral: TokenValue = self.collateral_calculator.calculate(account, cache)
all_open_orders: typing.Dict[str, OpenOrders] = {
str(oo_watcher.latest.address): oo_watcher.latest for oo_watcher in self.all_open_orders_watchers}
available_collateral: TokenValue = self.collateral_calculator.calculate(account, all_open_orders, cache)
base_value = account.net_assets[self.base_index]
if base_value is None:
@ -109,7 +114,7 @@ class PerpInventoryAccountWatcher:
raise Exception(
f"Could not find perp account for {self.market.symbol} in account {account.address} at index {self.perp_account_index}.")
available_collateral: TokenValue = self.collateral_calculator.calculate(account, cache)
available_collateral: TokenValue = self.collateral_calculator.calculate(account, {}, cache)
base_lots = perp_account.base_position
base_value = self.market.lot_size_converter.quantity_lots_to_value(base_lots)

View File

@ -185,7 +185,8 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder):
group_address: PublicKey,
cache_address: PublicKey,
account_address: PublicKey,
open_orders_address: PublicKey
open_orders_address: PublicKey,
all_open_orders_addresses: typing.Sequence[PublicKey]
):
super().__init__()
self.order_owner: PublicKey = order_owner
@ -196,6 +197,7 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder):
self.cache_address: PublicKey = cache_address
self.account_address: PublicKey = account_address
self.open_orders_address: PublicKey = open_orders_address
self.all_open_orders_addresses: typing.Sequence[PublicKey] = all_open_orders_addresses
self.collateral_calculator: mango.CollateralCalculator = mango.SpotCollateralCalculator()
@ -204,16 +206,33 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder):
self.group_address,
self.cache_address,
self.account_address,
self.open_orders_address,
self.market.underlying_serum_market.state.bids(),
self.market.underlying_serum_market.state.asks()
self.market.underlying_serum_market.state.asks(),
*self.all_open_orders_addresses
]
account_infos: typing.Sequence[mango.AccountInfo] = mango.AccountInfo.load_multiple(context, addresses)
group: mango.Group = mango.Group.parse(context, account_infos[0])
cache: mango.Cache = mango.Cache.parse(account_infos[1])
account: mango.Account = mango.Account.parse(account_infos[2], group)
placed_orders_container: mango.PlacedOrdersContainer = mango.OpenOrders.parse(
account_infos[3], self.market.base.decimals, self.market.quote.decimals)
# Update our stash of OpenOrders addresses for next time, in case new OpenOrders accounts were added
self.all_open_orders_addresses = list([oo for oo in account.spot_open_orders if oo is not None])
spot_open_orders_account_infos_by_address = {
str(account_info.address): account_info for account_info in account_infos[5:]}
all_open_orders: typing.Dict[str, mango.OpenOrders] = {}
for basket_token in account.basket:
if basket_token.spot_open_orders is not None and str(basket_token.spot_open_orders) in spot_open_orders_account_infos_by_address:
account_info: mango.AccountInfo = spot_open_orders_account_infos_by_address[str(
basket_token.spot_open_orders)]
open_orders: mango.OpenOrders = mango.OpenOrders.parse(
account_info,
basket_token.token_info.decimals,
account.shared_quote_token.token_info.token.decimals)
all_open_orders[str(basket_token.spot_open_orders)] = open_orders
placed_orders_container: mango.PlacedOrdersContainer = all_open_orders[str(self.open_orders_address)]
# Spot markets don't accrue MNGO liquidity incentives
mngo = account.group.find_token_info_by_symbol("MNGO").token
@ -222,7 +241,7 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder):
base_value = mango.TokenValue.find_by_symbol(account.net_assets, self.market.base.symbol)
quote_value = mango.TokenValue.find_by_symbol(account.net_assets, self.market.quote.symbol)
available_collateral: TokenValue = self.collateral_calculator.calculate(account, cache)
available_collateral: TokenValue = self.collateral_calculator.calculate(account, all_open_orders, cache)
inventory: mango.Inventory = mango.Inventory(mango.InventorySource.ACCOUNT,
mngo_accrued,
available_collateral,
@ -230,9 +249,9 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder):
quote_value)
bids: typing.Sequence[mango.Order] = mango.parse_account_info_to_orders(
account_infos[4], self.market.underlying_serum_market)
account_infos[3], self.market.underlying_serum_market)
asks: typing.Sequence[mango.Order] = mango.parse_account_info_to_orders(
account_infos[5], self.market.underlying_serum_market)
account_infos[4], self.market.underlying_serum_market)
price: mango.Price = self.oracle.fetch_price(context)
@ -289,7 +308,7 @@ class PerpPollingModelStateBuilder(PollingModelStateBuilder):
base_value = self.market.lot_size_converter.quantity_lots_to_value(base_lots)
base_token_value = mango.TokenValue(self.market.base, base_value)
quote_token_value = mango.TokenValue.find_by_symbol(account.net_assets, self.market.quote.symbol)
available_collateral: TokenValue = self.collateral_calculator.calculate(account, cache)
available_collateral: TokenValue = self.collateral_calculator.calculate(account, {}, cache)
inventory: mango.Inventory = mango.Inventory(mango.InventorySource.ACCOUNT,
perp_account.mngo_accrued,
available_collateral,

View File

@ -14,12 +14,12 @@
# [Email](mailto:hello@blockworks.foundation)
import enum
from mango.constants import SYSTEM_PROGRAM_ADDRESS
import mango
import typing
from solana.publickey import PublicKey
from ..constants import SYSTEM_PROGRAM_ADDRESS
from .modelstate import ModelState
from .modelstatebuilder import ModelStateBuilder, WebsocketModelStateBuilder, SerumPollingModelStateBuilder, SpotPollingModelStateBuilder, PerpPollingModelStateBuilder
@ -89,11 +89,13 @@ def _polling_spot_model_state_builder_factory(group: mango.Group, account: mango
oracle: mango.Oracle) -> ModelStateBuilder:
market_index: int = group.find_spot_market_index(market.address)
open_orders_address: typing.Optional[PublicKey] = account.spot_open_orders[market_index]
all_open_orders_addresses: typing.Sequence[PublicKey] = list(
[oo for oo in account.spot_open_orders if oo is not None])
if open_orders_address is None:
raise Exception(
f"Could not find spot openorders in account {account.address} for market {market.symbol}.")
return SpotPollingModelStateBuilder(
open_orders_address, 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, all_open_orders_addresses)
def _polling_perp_model_state_builder_factory(group: mango.Group, account: mango.Account, market: mango.PerpMarket,
@ -137,9 +139,24 @@ def _websocket_model_state_builder_factory(context: mango.Context, disposer: man
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)
inventory_watcher = mango.SpotInventoryAccountWatcher(market, latest_account_observer, cache_watcher)
latest_open_orders_observer = mango.build_spot_open_orders_watcher(
context, websocket_manager, health_check, wallet, account, group, market)
all_open_orders_watchers: typing.List[mango.Watcher[mango.OpenOrders]] = []
for basket_token in account.basket:
if basket_token.spot_open_orders is not None:
spot_market_symbol: str = f"spot:{basket_token.token_info.token.symbol}/{account.shared_quote_token.token_info.token.symbol}"
spot_market = context.market_lookup.find_by_symbol(spot_market_symbol)
if spot_market is None:
raise Exception(f"Could not find spot market {spot_market_symbol}")
if not isinstance(spot_market, mango.SpotMarket):
raise Exception(f"Market {spot_market_symbol} is not a spot market")
oo_watcher = mango.build_spot_open_orders_watcher(
context, websocket_manager, health_check, wallet, account, group, spot_market)
all_open_orders_watchers += [oo_watcher]
if market.base == spot_market.base and market.quote == spot_market.quote:
latest_open_orders_observer = oo_watcher
inventory_watcher = mango.SpotInventoryAccountWatcher(
market, latest_account_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(

View File

@ -81,7 +81,7 @@ def build_cache_watcher(context: Context, manager: WebSocketSubscriptionManager,
return latest_cache_observer
def build_spot_open_orders_watcher(context: Context, manager: WebSocketSubscriptionManager, health_check: HealthCheck, wallet: Wallet, account: Account, group: Group, spot_market: SpotMarket) -> Watcher[PlacedOrdersContainer]:
def build_spot_open_orders_watcher(context: Context, manager: WebSocketSubscriptionManager, health_check: HealthCheck, wallet: Wallet, account: Account, group: Group, spot_market: SpotMarket) -> Watcher[OpenOrders]:
market_index = group.find_spot_market_index(spot_market.address)
open_orders_address = account.spot_open_orders[market_index]
if open_orders_address is None:
@ -97,7 +97,7 @@ def build_spot_open_orders_watcher(context: Context, manager: WebSocketSubscript
manager.add(spot_open_orders_subscription)
initial_spot_open_orders = OpenOrders.load(
context, open_orders_address, spot_market.base.decimals, spot_market.quote.decimals)
latest_open_orders_observer = LatestItemObserverSubscriber[PlacedOrdersContainer](
latest_open_orders_observer = LatestItemObserverSubscriber[OpenOrders](
initial_spot_open_orders)
spot_open_orders_subscription.publisher.subscribe(latest_open_orders_observer)
health_check.add("open_orders_subscription", spot_open_orders_subscription.publisher)