# # ⚠ 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 = ( - 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} »"""