mango-explorer/mango/perpmarketdetails.py

291 lines
11 KiB
Python

# # ⚠ 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 typing
from datetime import datetime, timedelta
from decimal import Decimal
from solana.publickey import PublicKey
from .accountinfo import AccountInfo
from .addressableaccount import AddressableAccount
from .context import Context
from .datetimes import utc_now
from .group import GroupSlot, Group
from .instrumentvalue import InstrumentValue
from .layouts import layouts
from .metadata import Metadata
from .observables import Disposable
from .tokens import Instrument, Token
from .tokenbank import TokenBank
from .version import Version
from .websocketsubscription import (
WebSocketAccountSubscription,
WebSocketSubscriptionManager,
)
class LiquidityMiningInfo:
def __init__(
self,
version: Version,
rate: Decimal,
max_depth_bps: Decimal,
period_start: datetime,
target_period_length: timedelta,
mngo_left: InstrumentValue,
mngo_per_period: InstrumentValue,
) -> None:
self.version: Version = version
self.rate: Decimal = rate
self.max_depth_bps: Decimal = max_depth_bps
self.period_start: datetime = period_start
self.target_period_length: timedelta = target_period_length
self.mngo_left: InstrumentValue = mngo_left
self.mngo_per_period: InstrumentValue = mngo_per_period
@staticmethod
def from_layout(
layout: typing.Any, version: Version, mngo: Token
) -> "LiquidityMiningInfo":
rate: Decimal = layout.rate
max_depth_bps: Decimal = layout.max_depth_bps
period_start: datetime = layout.period_start
target_period_length: timedelta = timedelta(
seconds=float(layout.target_period_length)
)
mngo_left: InstrumentValue = InstrumentValue(
mngo, mngo.shift_to_decimals(layout.mngo_left)
)
mngo_per_period: InstrumentValue = InstrumentValue(
mngo, mngo.shift_to_decimals(layout.mngo_per_period)
)
return LiquidityMiningInfo(
version,
rate,
max_depth_bps,
period_start,
target_period_length,
mngo_left,
mngo_per_period,
)
def __str__(self) -> str:
# Some calculations here are basd on this message from 0xHiroku#0491 on Discord:
# https://discord.com/channels/791995070613159966/873184582948765736/889864341451599912
#
# // mngoLeft, mngoPerPeriod, periodStart, targetPeriodLength from PerpMarket.liquidityMiningInfo
#
# portion_given = 1 - mngoLeft / mngoPerPeriod
# elapsed = (<current_time> - periodStart) / targetPeriodLength
# est_next = elapsed / portion_given - elapsed
now: datetime = utc_now().replace(microsecond=0)
mngo_distributed: InstrumentValue = self.mngo_per_period - self.mngo_left
proportion_distributed: Decimal = Decimal(0)
elapsed: timedelta = now - self.period_start
elapsed_seconds: float = elapsed.total_seconds()
rounded_elapsed: timedelta = timedelta(seconds=int(elapsed_seconds))
estimated_duration_seconds: float = elapsed_seconds
estimated_duration: timedelta = timedelta(
seconds=int(estimated_duration_seconds)
)
estimated_remaining_seconds: float = (
estimated_duration_seconds - elapsed_seconds
)
estimated_remaining: timedelta = timedelta(
seconds=int(estimated_remaining_seconds)
)
estimated_end: datetime = now + estimated_remaining
if self.mngo_per_period.value != 0:
proportion_distributed = mngo_distributed.value / self.mngo_per_period.value
estimated_duration_seconds = elapsed_seconds / float(proportion_distributed)
estimated_duration = timedelta(seconds=int(estimated_duration_seconds))
estimated_remaining_seconds = estimated_duration_seconds - elapsed_seconds
estimated_remaining = timedelta(seconds=int(estimated_remaining_seconds))
estimated_end = now + estimated_remaining
return f"""« LiquidityMiningInfo {self.version}
Period Start : {self.period_start}
Period End (Est.): {estimated_end}
Target Duration : {self.target_period_length} hours
Elapsed : {rounded_elapsed} hours
Duration (Est.) : {estimated_duration} hours
Remaining (Est.) : {estimated_remaining} hours
Max Depth Bps : {self.max_depth_bps}
MNGO Per Period : {self.mngo_per_period}
MNGO Remaining : {self.mngo_left}
MNGO Distributed : {mngo_distributed}
% Distributed : {proportion_distributed:.2%}
Rate : {self.rate}
»"""
# # 🥭 PerpMarketDetails class
#
# `PerpMarketDetails` holds details of a particular perp market.
#
class PerpMarketDetails(AddressableAccount):
def __init__(
self,
account_info: AccountInfo,
version: Version,
meta_data: Metadata,
group: Group,
bids: PublicKey,
asks: PublicKey,
event_queue: PublicKey,
base_lot_size: Decimal,
quote_lot_size: Decimal,
long_funding: Decimal,
short_funding: Decimal,
open_interest: Decimal,
last_updated: datetime,
seq_num: Decimal,
fees_accrued: Decimal,
liquidity_mining_info: LiquidityMiningInfo,
mngo_vault: PublicKey,
) -> None:
super().__init__(account_info)
self.version: Version = version
self.meta_data: Metadata = meta_data
self.group: Group = group
self.bids: PublicKey = bids
self.asks: PublicKey = asks
self.event_queue: PublicKey = event_queue
self.base_lot_size: Decimal = base_lot_size
self.quote_lot_size: Decimal = quote_lot_size
self.long_funding: Decimal = long_funding
self.short_funding: Decimal = short_funding
self.open_interest: Decimal = open_interest
self.last_updated: datetime = last_updated
self.seq_num: Decimal = seq_num
self.fees_accrued: Decimal = fees_accrued
self.liquidity_mining_info: LiquidityMiningInfo = liquidity_mining_info
self.mngo_vault: PublicKey = mngo_vault
slot: GroupSlot = group.slot_by_perp_market_address(self.address)
if slot is None:
raise Exception(
f"Could not find slot for perp market {self.address} in group {group.address}."
)
self.market_index: int = slot.index
self.base_instrument: Instrument = slot.base_instrument
self.base_token: typing.Optional[TokenBank] = slot.base_token_bank
self.quote_token: TokenBank = group.shared_quote
@staticmethod
def from_layout(
layout: typing.Any, account_info: AccountInfo, version: Version, group: Group
) -> "PerpMarketDetails":
meta_data = Metadata.from_layout(layout.meta_data)
bids: PublicKey = layout.bids
asks: PublicKey = layout.asks
event_queue: PublicKey = layout.event_queue
base_lot_size: Decimal = layout.base_lot_size
quote_lot_size: Decimal = layout.quote_lot_size
long_funding: Decimal = layout.long_funding
short_funding: Decimal = layout.short_funding
open_interest: Decimal = layout.open_interest
last_updated: datetime = layout.last_updated
seq_num: Decimal = layout.seq_num
fees_accrued: Decimal = layout.fees_accrued
liquidity_mining_info: LiquidityMiningInfo = LiquidityMiningInfo.from_layout(
layout.liquidity_mining_info, Version.V1, group.liquidity_incentive_token
)
mngo_vault: PublicKey = layout.mngo_vault
return PerpMarketDetails(
account_info,
version,
meta_data,
group,
bids,
asks,
event_queue,
base_lot_size,
quote_lot_size,
long_funding,
short_funding,
open_interest,
last_updated,
seq_num,
fees_accrued,
liquidity_mining_info,
mngo_vault,
)
@staticmethod
def parse(account_info: AccountInfo, group: Group) -> "PerpMarketDetails":
data = account_info.data
if len(data) != layouts.PERP_MARKET.sizeof():
raise Exception(
f"PerpMarketDetails data length ({len(data)}) does not match expected size ({layouts.PERP_MARKET.sizeof()})"
)
layout = layouts.PERP_MARKET.parse(data)
return PerpMarketDetails.from_layout(layout, account_info, Version.V1, group)
@staticmethod
def load(context: Context, address: PublicKey, group: Group) -> "PerpMarketDetails":
account_info = AccountInfo.load(context, address)
if account_info is None:
raise Exception(
f"PerpMarketDetails account not found at address '{address}'"
)
return PerpMarketDetails.parse(account_info, group)
def subscribe(
self,
context: Context,
websocketmanager: WebSocketSubscriptionManager,
callback: typing.Callable[["PerpMarketDetails"], None],
) -> Disposable:
def __parser(account_info: AccountInfo) -> PerpMarketDetails:
return PerpMarketDetails.parse(account_info, self.group)
subscription = WebSocketAccountSubscription(context, self.address, __parser)
websocketmanager.add(subscription)
subscription.publisher.subscribe(on_next=callback) # type: ignore[call-arg]
return subscription
def __str__(self) -> str:
liquidity_mining_info: str = f"{self.liquidity_mining_info}".replace(
"\n", "\n "
)
return f"""« PerpMarketDetails {self.version} [{self.address}]
{self.meta_data}
Group: {self.group.address}
Bids: {self.bids}
Asks: {self.asks}
Event Queue: {self.event_queue}
Long Funding: {self.long_funding}
Short Funding: {self.short_funding}
Open Interest: {self.open_interest}
Base Lot Size: {self.base_lot_size}
Quote Lot Size: {self.quote_lot_size}
Last Updated: {self.last_updated}
Seq Num: {self.seq_num}
Fees Accrued: {self.fees_accrued}
MNGO Vault: {self.mngo_vault}
{liquidity_mining_info}
»"""