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 .account import Account
from .cache import Cache, PriceCache from .cache import Cache, PriceCache
from .openorders import OpenOrders
from .spotmarketinfo import SpotMarketInfo from .spotmarketinfo import SpotMarketInfo
from .tokenvalue import TokenValue from .tokenvalue import TokenValue
@ -29,7 +30,7 @@ class CollateralCalculator(metaclass=abc.ABCMeta):
def __init__(self): def __init__(self):
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__) 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.") raise NotImplementedError("CollateralCalculator.calculate() is not implemented on the base type.")
@ -37,7 +38,7 @@ class SerumCollateralCalculator(CollateralCalculator):
def __init__(self): def __init__(self):
super().__init__() 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.") 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): # 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 # 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: # Quote token calculation:
# total_collateral = deposits[QUOTE_INDEX] * deposit_index - borrows[QUOTE_INDEX] * borrow_index # 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. # Note: the `AccountBasketToken` in the `Account` already factors the deposit and borrow index.
@ -70,12 +71,18 @@ class SpotCollateralCalculator(CollateralCalculator):
raise Exception( raise Exception(
f"Could not read spot market of token {basket_token.token_info.token.symbol} at index {index} of cache at {cache.address}") 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: # Base token calculations:
# total_collateral += prices[i] * (init_asset_weights[i] * deposits[i] * deposit_index - init_liab_weights[i] * borrows[i] * borrow_index) # 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. # 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.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 total += weighted
return TokenValue(account.group.shared_quote_token.token, total) 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): # 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 # 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: # Quote token calculation:
# total_collateral = deposits[QUOTE_INDEX] * deposit_index - borrows[QUOTE_INDEX] * borrow_index # 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. # Note: the `AccountBasketToken` in the `Account` already factors the deposit and borrow index.

View File

@ -15,6 +15,7 @@
import logging import logging
import typing
from decimal import Decimal from decimal import Decimal
@ -23,6 +24,7 @@ from .cache import Cache
from .collateralcalculator import CollateralCalculator, SpotCollateralCalculator, PerpCollateralCalculator from .collateralcalculator import CollateralCalculator, SpotCollateralCalculator, PerpCollateralCalculator
from .group import Group from .group import Group
from .market import InventorySource, Market from .market import InventorySource, Market
from .openorders import OpenOrders
from .perpmarket import PerpMarket from .perpmarket import PerpMarket
from .tokenvalue import TokenValue from .tokenvalue import TokenValue
from .watcher import Watcher from .watcher import Watcher
@ -56,8 +58,9 @@ class Inventory:
class SpotInventoryAccountWatcher: 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.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 self.cache_watcher: Watcher[Cache] = cache_watcher
account: Account = account_watcher.latest account: Account = account_watcher.latest
base_value = TokenValue.find_by_symbol(account.net_assets, market.base.symbol) 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 = account.group.find_token_info_by_symbol("MNGO").token
mngo_accrued: TokenValue = TokenValue(mngo, Decimal(0)) 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] base_value = account.net_assets[self.base_index]
if base_value is None: if base_value is None:
@ -109,7 +114,7 @@ class PerpInventoryAccountWatcher:
raise Exception( raise Exception(
f"Could not find perp account for {self.market.symbol} in account {account.address} at index {self.perp_account_index}.") 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_lots = perp_account.base_position
base_value = self.market.lot_size_converter.quantity_lots_to_value(base_lots) 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, group_address: PublicKey,
cache_address: PublicKey, cache_address: PublicKey,
account_address: PublicKey, account_address: PublicKey,
open_orders_address: PublicKey open_orders_address: PublicKey,
all_open_orders_addresses: typing.Sequence[PublicKey]
): ):
super().__init__() super().__init__()
self.order_owner: PublicKey = order_owner self.order_owner: PublicKey = order_owner
@ -196,6 +197,7 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder):
self.cache_address: PublicKey = cache_address self.cache_address: PublicKey = cache_address
self.account_address: PublicKey = account_address self.account_address: PublicKey = account_address
self.open_orders_address: PublicKey = open_orders_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() self.collateral_calculator: mango.CollateralCalculator = mango.SpotCollateralCalculator()
@ -204,16 +206,33 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder):
self.group_address, self.group_address,
self.cache_address, self.cache_address,
self.account_address, self.account_address,
self.open_orders_address,
self.market.underlying_serum_market.state.bids(), 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) 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])
cache: mango.Cache = mango.Cache.parse(account_infos[1]) cache: mango.Cache = mango.Cache.parse(account_infos[1])
account: mango.Account = mango.Account.parse(account_infos[2], group) 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 # Spot markets don't accrue MNGO liquidity incentives
mngo = account.group.find_token_info_by_symbol("MNGO").token 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) 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) 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, inventory: mango.Inventory = mango.Inventory(mango.InventorySource.ACCOUNT,
mngo_accrued, mngo_accrued,
available_collateral, available_collateral,
@ -230,9 +249,9 @@ class SpotPollingModelStateBuilder(PollingModelStateBuilder):
quote_value) quote_value)
bids: typing.Sequence[mango.Order] = mango.parse_account_info_to_orders( 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( 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) 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_value = self.market.lot_size_converter.quantity_lots_to_value(base_lots)
base_token_value = mango.TokenValue(self.market.base, base_value) 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) 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, inventory: mango.Inventory = mango.Inventory(mango.InventorySource.ACCOUNT,
perp_account.mngo_accrued, perp_account.mngo_accrued,
available_collateral, available_collateral,

View File

@ -14,12 +14,12 @@
# [Email](mailto:hello@blockworks.foundation) # [Email](mailto:hello@blockworks.foundation)
import enum import enum
from mango.constants import SYSTEM_PROGRAM_ADDRESS
import mango import mango
import typing import typing
from solana.publickey import PublicKey from solana.publickey import PublicKey
from ..constants import SYSTEM_PROGRAM_ADDRESS
from .modelstate import ModelState from .modelstate import ModelState
from .modelstatebuilder import ModelStateBuilder, WebsocketModelStateBuilder, SerumPollingModelStateBuilder, SpotPollingModelStateBuilder, PerpPollingModelStateBuilder 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: oracle: mango.Oracle) -> ModelStateBuilder:
market_index: int = group.find_spot_market_index(market.address) market_index: int = group.find_spot_market_index(market.address)
open_orders_address: typing.Optional[PublicKey] = account.spot_open_orders[market_index] 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: if open_orders_address is None:
raise Exception( raise Exception(
f"Could not find spot openorders in account {account.address} for market {market.symbol}.") f"Could not find spot openorders in account {account.address} for market {market.symbol}.")
return SpotPollingModelStateBuilder( 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, 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: mango.Cache = mango.Cache.load(context, group.cache)
cache_watcher: mango.Watcher[mango.Cache] = mango.build_cache_watcher( cache_watcher: mango.Watcher[mango.Cache] = mango.build_cache_watcher(
context, websocket_manager, health_check, cache, group) 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( all_open_orders_watchers: typing.List[mango.Watcher[mango.OpenOrders]] = []
context, websocket_manager, health_check, wallet, account, group, market) 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( latest_bids_watcher = mango.build_serum_orderbook_side_watcher(
context, websocket_manager, health_check, market.underlying_serum_market, mango.OrderBookSideType.BIDS) context, websocket_manager, health_check, market.underlying_serum_market, mango.OrderBookSideType.BIDS)
latest_asks_watcher = mango.build_serum_orderbook_side_watcher( 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 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) market_index = group.find_spot_market_index(spot_market.address)
open_orders_address = account.spot_open_orders[market_index] open_orders_address = account.spot_open_orders[market_index]
if open_orders_address is None: 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) manager.add(spot_open_orders_subscription)
initial_spot_open_orders = OpenOrders.load( initial_spot_open_orders = OpenOrders.load(
context, open_orders_address, spot_market.base.decimals, spot_market.quote.decimals) 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) initial_spot_open_orders)
spot_open_orders_subscription.publisher.subscribe(latest_open_orders_observer) spot_open_orders_subscription.publisher.subscribe(latest_open_orders_observer)
health_check.add("open_orders_subscription", spot_open_orders_subscription.publisher) health_check.add("open_orders_subscription", spot_open_orders_subscription.publisher)