2021-06-25 02:33:40 -07:00
|
|
|
|
# # ⚠ 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)
|
|
|
|
|
|
2021-09-06 09:07:26 -07:00
|
|
|
|
import rx
|
|
|
|
|
import rx.disposable
|
|
|
|
|
import rx.subject
|
|
|
|
|
import rx.operators as ops
|
2021-07-23 06:18:26 -07:00
|
|
|
|
import typing
|
|
|
|
|
|
2021-11-15 12:39:29 -08:00
|
|
|
|
from datetime import datetime
|
|
|
|
|
from dateutil import parser
|
|
|
|
|
from decimal import Decimal
|
2021-06-25 02:33:40 -07:00
|
|
|
|
from solana.publickey import PublicKey
|
|
|
|
|
|
|
|
|
|
from .accountinfo import AccountInfo
|
|
|
|
|
from .context import Context
|
2021-06-25 07:50:37 -07:00
|
|
|
|
from .group import Group
|
2021-09-06 09:07:26 -07:00
|
|
|
|
from .loadedmarket import LoadedMarket
|
2021-08-22 11:48:20 -07:00
|
|
|
|
from .lotsizeconverter import LotSizeConverter, RaisingLotSizeConverter
|
2021-07-23 06:18:26 -07:00
|
|
|
|
from .market import Market, InventorySource
|
2021-09-06 09:07:26 -07:00
|
|
|
|
from .observables import DisposingSubject, observable_pipeline_error_reporter
|
2021-08-21 14:06:58 -07:00
|
|
|
|
from .orderbookside import PerpOrderBookSide
|
2021-07-23 06:18:26 -07:00
|
|
|
|
from .orders import Order
|
2021-09-06 09:07:26 -07:00
|
|
|
|
from .perpeventqueue import PerpEvent, PerpEventQueue, UnseenPerpEventChangesTracker
|
2021-07-23 06:18:26 -07:00
|
|
|
|
from .perpmarketdetails import PerpMarketDetails
|
2021-11-08 03:39:09 -08:00
|
|
|
|
from .token import Instrument, Token
|
2021-07-23 06:18:26 -07:00
|
|
|
|
|
2021-06-25 02:33:40 -07:00
|
|
|
|
|
2021-11-15 12:39:29 -08:00
|
|
|
|
# # 🥭 FundingRate class
|
|
|
|
|
#
|
|
|
|
|
# A simple way to package details of a funding rate in a single object.
|
|
|
|
|
#
|
|
|
|
|
class FundingRate(typing.NamedTuple):
|
|
|
|
|
symbol: str
|
|
|
|
|
rate: Decimal
|
|
|
|
|
oracle_price: Decimal
|
|
|
|
|
open_interest: Decimal
|
|
|
|
|
from_: datetime
|
|
|
|
|
to: datetime
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def from_stats_data(symbol: str, lot_size_converter: LotSizeConverter, oldest_stats: typing.Dict[str, typing.Any], newest_stats: typing.Dict[str, typing.Any]) -> "FundingRate":
|
|
|
|
|
oldest_short_funding = Decimal(oldest_stats["shortFunding"])
|
|
|
|
|
oldest_long_funding = Decimal(oldest_stats["longFunding"])
|
|
|
|
|
oldest_oracle_price = Decimal(oldest_stats["baseOraclePrice"])
|
|
|
|
|
from_timestamp = parser.parse(oldest_stats["time"]).replace(microsecond=0)
|
|
|
|
|
|
|
|
|
|
newest_short_funding = Decimal(newest_stats["shortFunding"])
|
|
|
|
|
newest_long_funding = Decimal(newest_stats["longFunding"])
|
|
|
|
|
newest_oracle_price = Decimal(newest_stats["baseOraclePrice"])
|
|
|
|
|
to_timestamp = parser.parse(newest_stats["time"]).replace(microsecond=0)
|
|
|
|
|
raw_open_interest = Decimal(newest_stats["openInterest"])
|
|
|
|
|
open_interest = lot_size_converter.base_size_lots_to_number(raw_open_interest) / 2
|
|
|
|
|
|
|
|
|
|
average_oracle_price = (oldest_oracle_price + newest_oracle_price) / 2
|
|
|
|
|
average_oracle_price = newest_oracle_price
|
|
|
|
|
|
|
|
|
|
start_funding = (oldest_long_funding + oldest_short_funding) / 2
|
|
|
|
|
end_funding = (newest_long_funding + newest_short_funding) / 2
|
|
|
|
|
funding_difference = end_funding - start_funding
|
|
|
|
|
|
|
|
|
|
funding_in_quote_decimals = lot_size_converter.quote.shift_to_decimals(funding_difference)
|
|
|
|
|
|
|
|
|
|
base_price_in_base_lots = average_oracle_price * lot_size_converter.lot_size
|
|
|
|
|
funding_rate = funding_in_quote_decimals / base_price_in_base_lots
|
|
|
|
|
return FundingRate(symbol=symbol, rate=funding_rate, oracle_price=average_oracle_price, open_interest=open_interest, from_=from_timestamp, to=to_timestamp)
|
|
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
|
return f"« 𝙵𝚞𝚗𝚍𝚒𝚗𝚐𝚁𝚊𝚝𝚎 {self.symbol} {self.rate:,.8%}, open interest: {self.open_interest:,.8f} from: {self.from_} to {self.to} »"
|
|
|
|
|
|
|
|
|
|
def __repr__(self) -> str:
|
|
|
|
|
return f"{self}"
|
|
|
|
|
|
|
|
|
|
|
2021-06-25 02:33:40 -07:00
|
|
|
|
# # 🥭 PerpMarket class
|
|
|
|
|
#
|
2021-07-23 06:18:26 -07:00
|
|
|
|
# This class encapsulates our knowledge of a Mango perps market.
|
|
|
|
|
#
|
2021-09-06 09:07:26 -07:00
|
|
|
|
class PerpMarket(LoadedMarket):
|
2021-11-09 05:23:36 -08:00
|
|
|
|
def __init__(self, mango_program_address: PublicKey, address: PublicKey, base: Instrument, quote: Token,
|
|
|
|
|
underlying_perp_market: PerpMarketDetails) -> None:
|
2021-08-26 02:31:02 -07:00
|
|
|
|
super().__init__(mango_program_address, address, InventorySource.ACCOUNT, base, quote, RaisingLotSizeConverter())
|
2021-07-23 06:18:26 -07:00
|
|
|
|
self.underlying_perp_market: PerpMarketDetails = underlying_perp_market
|
2021-07-28 09:43:58 -07:00
|
|
|
|
self.lot_size_converter: LotSizeConverter = LotSizeConverter(
|
|
|
|
|
base, underlying_perp_market.base_lot_size, quote, underlying_perp_market.quote_lot_size)
|
2021-07-23 06:18:26 -07:00
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def symbol(self) -> str:
|
|
|
|
|
return f"{self.base.symbol}-PERP"
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def group(self) -> Group:
|
|
|
|
|
return self.underlying_perp_market.group
|
|
|
|
|
|
2021-10-26 10:45:04 -07:00
|
|
|
|
@property
|
|
|
|
|
def bids_address(self) -> PublicKey:
|
|
|
|
|
return self.underlying_perp_market.bids
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def asks_address(self) -> PublicKey:
|
|
|
|
|
return self.underlying_perp_market.asks
|
|
|
|
|
|
|
|
|
|
def parse_account_info_to_orders(self, account_info: AccountInfo) -> typing.Sequence[Order]:
|
|
|
|
|
side: PerpOrderBookSide = PerpOrderBookSide.parse(account_info, self.underlying_perp_market)
|
|
|
|
|
return side.orders()
|
|
|
|
|
|
2021-11-15 12:39:29 -08:00
|
|
|
|
def fetch_funding(self, context: Context) -> FundingRate:
|
|
|
|
|
stats = context.fetch_stats(f"perp/funding_rate?mangoGroup={self.group.name}&market={self.symbol}")
|
|
|
|
|
newest_stats = stats[0]
|
|
|
|
|
oldest_stats = stats[-1]
|
|
|
|
|
|
|
|
|
|
return FundingRate.from_stats_data(self.symbol, self.lot_size_converter, oldest_stats, newest_stats)
|
|
|
|
|
|
2021-07-23 06:18:26 -07:00
|
|
|
|
def unprocessed_events(self, context: Context) -> typing.Sequence[PerpEvent]:
|
2021-07-28 09:43:58 -07:00
|
|
|
|
event_queue: PerpEventQueue = PerpEventQueue.load(
|
|
|
|
|
context, self.underlying_perp_market.event_queue, self.lot_size_converter)
|
2021-08-01 09:47:45 -07:00
|
|
|
|
return event_queue.unprocessed_events
|
2021-07-23 06:18:26 -07:00
|
|
|
|
|
|
|
|
|
def accounts_to_crank(self, context: Context, additional_account_to_crank: typing.Optional[PublicKey]) -> typing.Sequence[PublicKey]:
|
|
|
|
|
accounts_to_crank: typing.List[PublicKey] = []
|
|
|
|
|
for event_to_crank in self.unprocessed_events(context):
|
|
|
|
|
accounts_to_crank += event_to_crank.accounts_to_crank
|
|
|
|
|
|
|
|
|
|
if additional_account_to_crank is not None:
|
|
|
|
|
accounts_to_crank += [additional_account_to_crank]
|
|
|
|
|
|
|
|
|
|
seen = []
|
|
|
|
|
distinct = []
|
|
|
|
|
for account in accounts_to_crank:
|
|
|
|
|
account_str = account.to_base58()
|
|
|
|
|
if account_str not in seen:
|
|
|
|
|
distinct += [account]
|
|
|
|
|
seen += [account_str]
|
|
|
|
|
distinct.sort(key=lambda address: address._key or [0])
|
|
|
|
|
return distinct
|
|
|
|
|
|
2021-09-06 09:07:26 -07:00
|
|
|
|
def observe_events(self, context: Context, interval: int = 30) -> DisposingSubject:
|
|
|
|
|
perp_event_queue: PerpEventQueue = PerpEventQueue.load(
|
|
|
|
|
context, self.underlying_perp_market.event_queue, self.lot_size_converter)
|
|
|
|
|
perp_splitter: UnseenPerpEventChangesTracker = UnseenPerpEventChangesTracker(perp_event_queue)
|
|
|
|
|
|
|
|
|
|
fill_events = DisposingSubject()
|
|
|
|
|
disposable_subscription = rx.interval(interval).pipe(
|
|
|
|
|
ops.observe_on(context.create_thread_pool_scheduler()),
|
|
|
|
|
ops.start_with(-1),
|
|
|
|
|
ops.map(lambda _: PerpEventQueue.load(
|
|
|
|
|
context, self.underlying_perp_market.event_queue, self.lot_size_converter)),
|
|
|
|
|
ops.flat_map(perp_splitter.unseen),
|
|
|
|
|
ops.catch(observable_pipeline_error_reporter),
|
|
|
|
|
ops.retry()
|
|
|
|
|
).subscribe(fill_events)
|
|
|
|
|
fill_events.add_disposable(disposable_subscription)
|
|
|
|
|
return fill_events
|
|
|
|
|
|
2021-07-23 06:18:26 -07:00
|
|
|
|
def __str__(self) -> str:
|
2021-07-28 09:43:58 -07:00
|
|
|
|
underlying: str = f"{self.underlying_perp_market}".replace("\n", "\n ")
|
2021-08-26 02:31:02 -07:00
|
|
|
|
return f"""« 𝙿𝚎𝚛𝚙𝙼𝚊𝚛𝚔𝚎𝚝 {self.symbol} {self.address} [{self.program_address}]
|
2021-07-28 09:43:58 -07:00
|
|
|
|
{underlying}
|
|
|
|
|
»"""
|
2021-07-23 06:18:26 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# # 🥭 PerpMarketStub class
|
2021-06-25 02:33:40 -07:00
|
|
|
|
#
|
2021-07-23 06:18:26 -07:00
|
|
|
|
# This class holds information to load a `PerpMarket` object but doesn't automatically load it.
|
|
|
|
|
#
|
|
|
|
|
class PerpMarketStub(Market):
|
2021-11-09 05:23:36 -08:00
|
|
|
|
def __init__(self, mango_program_address: PublicKey, address: PublicKey, base: Instrument, quote: Token,
|
|
|
|
|
group_address: PublicKey) -> None:
|
2021-08-26 02:31:02 -07:00
|
|
|
|
super().__init__(mango_program_address, address, InventorySource.ACCOUNT, base, quote, RaisingLotSizeConverter())
|
2021-07-23 06:18:26 -07:00
|
|
|
|
self.group_address: PublicKey = group_address
|
|
|
|
|
|
|
|
|
|
def load(self, context: Context, group: typing.Optional[Group] = None) -> PerpMarket:
|
|
|
|
|
actual_group: Group = group or Group.load(context, self.group_address)
|
|
|
|
|
underlying_perp_market: PerpMarketDetails = PerpMarketDetails.load(context, self.address, actual_group)
|
2021-08-26 02:31:02 -07:00
|
|
|
|
return PerpMarket(self.program_address, self.address, self.base, self.quote, underlying_perp_market)
|
2021-06-25 02:33:40 -07:00
|
|
|
|
|
2021-07-23 06:18:26 -07:00
|
|
|
|
@property
|
|
|
|
|
def symbol(self) -> str:
|
|
|
|
|
return f"{self.base.symbol}-PERP"
|
2021-06-25 02:33:40 -07:00
|
|
|
|
|
2021-07-23 06:18:26 -07:00
|
|
|
|
def __str__(self) -> str:
|
2021-08-26 02:31:02 -07:00
|
|
|
|
return f"« 𝙿𝚎𝚛𝚙𝙼𝚊𝚛𝚔𝚎𝚝𝚂𝚝𝚞𝚋 {self.symbol} {self.address} [{self.program_address}] »"
|