From 9ab1e014176cf659839e6dc10301c50eb7e0c3e8 Mon Sep 17 00:00:00 2001 From: frank <98238480+soundsonacid@users.noreply.github.com> Date: Tue, 30 Jan 2024 16:04:42 -0800 Subject: [PATCH] frank/misc for perp filler (#88) * fix: misc math fixes, dlob fixes * more misc perp filler stuff * chore: increase usermap timeout * wip misc fixes * fix: more misc fixes * more misc perp filler fixes and additions * remove debug prints so i don't forget * fix: misc fixes * make get perp with lp settle pythonic * pr comments --- src/driftpy/accounts/get_accounts.py | 1 + src/driftpy/accounts/oracle.py | 2 +- src/driftpy/dlob/dlob.py | 100 ++++++++-- src/driftpy/drift_client.py | 187 ++++++++++++++++++ src/driftpy/drift_user.py | 142 ++++++------- src/driftpy/math/amm.py | 7 +- src/driftpy/math/exchange_status.py | 19 +- src/driftpy/math/funding.py | 23 ++- src/driftpy/math/perp_position.py | 2 +- src/driftpy/math/utils.py | 15 +- .../priority_fees/priority_fee_subscriber.py | 2 +- src/driftpy/types.py | 14 +- tests/amm.py | 12 +- 13 files changed, 421 insertions(+), 105 deletions(-) diff --git a/src/driftpy/accounts/get_accounts.py b/src/driftpy/accounts/get_accounts.py index 9835e769..fb5602a8 100644 --- a/src/driftpy/accounts/get_accounts.py +++ b/src/driftpy/accounts/get_accounts.py @@ -78,6 +78,7 @@ async def get_user_account( ) -> UserAccount: return (await get_user_account_and_slot(program, user_public_key)).data + async def get_perp_market_account_and_slot( program: Program, market_index: int ) -> Optional[DataAndSlot[PerpMarketAccount]]: diff --git a/src/driftpy/accounts/oracle.py b/src/driftpy/accounts/oracle.py index 64d14ca5..fef884cb 100644 --- a/src/driftpy/accounts/oracle.py +++ b/src/driftpy/accounts/oracle.py @@ -91,7 +91,7 @@ def decode_pyth_price_info( confidence=convert_pyth_price(pyth_price_info.confidence_interval, scale), twap=convert_pyth_price(twap, scale), twap_confidence=convert_pyth_price(twac, scale), - has_sufficient_number_of_datapoints=True, + has_sufficient_number_of_data_points=True, ) diff --git a/src/driftpy/dlob/dlob.py b/src/driftpy/dlob/dlob.py index e72b9f91..bdda4356 100644 --- a/src/driftpy/dlob/dlob.py +++ b/src/driftpy/dlob/dlob.py @@ -1,5 +1,5 @@ import copy -from typing import Callable, Dict, Generator, List, Optional, Union +from typing import Callable, Dict, Generator, List, Optional, Tuple, Union from solders.pubkey import Pubkey from driftpy.constants.numeric_constants import ( @@ -825,9 +825,8 @@ def find_taking_nodes_to_fill( taking_order_generator = self.get_taking_asks( market_index, market_type, slot, oracle_price_data ) - maker_node_generator_fn = lambda: self.get_resting_limit_bids( - market_index, slot, market_type, oracle_price_data - ) + maker_node_generator_fn = self.get_resting_limit_bids + does_cross = ( lambda taker_price, maker_price: ( taker_price is None or taker_price <= maker_price @@ -872,9 +871,8 @@ def does_cross(price: Optional[int] = None) -> bool: taking_order_generator = self.get_taking_bids( market_index, market_type, slot, oracle_price_data ) - maker_node_generator_fn = lambda: self.get_resting_limit_asks( - market_index, slot, market_type, oracle_price_data - ) + maker_node_generator_fn = self.get_resting_limit_asks + does_cross = ( lambda taker_price, maker_price: ( taker_price is None or taker_price >= maker_price @@ -917,6 +915,86 @@ def does_cross(price: Optional[int] = None) -> bool: return nodes_to_fill + def determine_maker_and_taker( + self, ask: DLOBNode, bid: DLOBNode + ) -> Union[Tuple[DLOBNode, DLOBNode], None]: + ask_slot = ask.order.slot + ask.order.auction_duration + bid_slot = bid.order.slot + bid.order.auction_duration + + if bid.order.post_only and ask.order.post_only: + return None + elif bid.order.post_only: + return (ask, bid) + elif ask.order.post_only: + return (bid, ask) + elif ask_slot <= bid_slot: + return (bid, ask) + else: + return (ask, bid) + + def find_crossing_resting_limit_orders( + self, + market_index: int, + slot: int, + market_type: MarketType, + oracle_price_data: OraclePriceData, + ) -> List[NodeToFill]: + nodes_to_fill: List[NodeToFill] = [] + + for ask in self.get_resting_limit_asks( + market_index, slot, market_type, oracle_price_data + ): + bid_generator = self.get_resting_limit_bids( + market_index, slot, market_type, oracle_price_data + ) + + for bid in bid_generator: + bid_price = bid.get_price(oracle_price_data, slot) + ask_price = ask.get_price(oracle_price_data, slot) + + # don't cross + if bid_price < ask_price: + break + + bid_order = bid.order + ask_order = ask.order + + # can't match from same user + if bid.user_account == ask.user_account: + break + + maker_and_taker = self.determine_maker_and_taker(ask, bid) + + # unable to match maker and taker due to post only or slot + if not maker_and_taker: + continue + + taker, maker = maker_and_taker + + bid_base_remaining = ( + bid_order.base_asset_amount - bid_order.base_asset_amount_filled + ) + ask_base_remaining = ( + ask_order.base_asset_amount - ask_order.base_asset_amount_filled + ) + + base_filled = min(bid_base_remaining, ask_base_remaining) + + new_bid = copy.deepcopy(bid_order) + new_bid.base_asset_amount_filled += base_filled + self.get_list_for_order(new_bid, slot).update(new_bid, bid.user_account) + + new_ask = copy.deepcopy(ask_order) + new_ask.base_asset_amount_filled += base_filled + self.get_list_for_order(new_ask, slot).update(new_ask, ask.user_account) + + nodes_to_fill.append(NodeToFill(taker, [maker])) + + if new_ask.base_asset_amount == new_ask.base_asset_amount_filled: + break + + return nodes_to_fill + def find_resting_limit_order_nodes_to_fill( self, market_index: int, @@ -1014,7 +1092,7 @@ def find_expired_nodes_to_fill( for ask_generator in ask_generators: for ask in ask_generator: - if is_order_expired(bid.order, ts, True): + if is_order_expired(ask.order, ts, True): nodes_to_fill.append(NodeToFill(ask, [])) return nodes_to_fill @@ -1040,9 +1118,9 @@ def merge_nodes_to_fill_helper(nodes_to_fill_list): node_to_fill.node, [] ) - if node_to_fill.maker_nodes is not None: - merged_nodes_to_fill[node_signature].maker_nodes.extend( - node_to_fill.maker_nodes + if node_to_fill.maker is not None: + merged_nodes_to_fill[node_signature].maker.extend( + node_to_fill.maker ) merge_nodes_to_fill_helper(resting_limit_order_nodes_to_fill) diff --git a/src/driftpy/drift_client.py b/src/driftpy/drift_client.py index 1c6970f7..f2413280 100644 --- a/src/driftpy/drift_client.py +++ b/src/driftpy/drift_client.py @@ -7,6 +7,7 @@ from solders.instruction import Instruction from solders.system_program import ID from solders.sysvar import RENT +from solders.signature import Signature from solders.address_lookup_table_account import AddressLookupTableAccount from solana.rpc.async_api import AsyncClient from solana.rpc.types import TxOpts @@ -1613,6 +1614,17 @@ def get_settle_pnl_ix( return instruction + def get_settle_pnl_ixs( + self, users: dict[Pubkey, UserAccount], market_indexes: list[int] + ) -> list[Instruction]: + ixs: list[Instruction] = [] + for pubkey, account in users.items(): + for market_index in market_indexes: + ix = self.get_settle_pnl_ix(pubkey, account, market_index) + ixs.append(ix) + + return ixs + async def resolve_spot_bankruptcy( self, user_authority: Pubkey, @@ -2040,6 +2052,181 @@ def get_initialize_insurance_fund_stake_ix( ), ) + async def get_fill_perp_order_ix( + self, + user_account_pubkey: Pubkey, + user_account: UserAccount, + order: Order, + maker_info: Optional[Union[MakerInfo, list[MakerInfo]]], + referrer_info: Optional[ReferrerInfo], + ) -> Instruction: + user_stats_pubkey = get_user_stats_account_public_key( + self.program.program_id, user_account.authority + ) + + filler_pubkey = self.get_user_account_public_key() + filler_stats_pubkey = self.get_user_stats_public_key() + + market_index = ( + order.market_index + if order + else next( + ( + order.market_index + for order in user_account.orders + if order.order_id == user_account.next_order_id - 1 + ), + None, + ) + ) + + maker_info = ( + maker_info + if isinstance(maker_info, list) + else [maker_info] + if maker_info + else [] + ) + + user_accounts = [user_account] + for maker in maker_info: + user_accounts.append(maker.maker_user_account) + + remaining_accounts = self.get_remaining_accounts(user_accounts, [market_index]) + + for maker in maker_info: + remaining_accounts.append( + AccountMeta(pubkey=maker.maker, is_writable=True, is_signer=False) + ) + remaining_accounts.append( + AccountMeta(pubkey=maker.maker_stats, is_writable=True, is_signer=False) + ) + + if referrer_info: + referrer_is_maker = any( + maker.maker == referrer_info.referrer for maker in maker_info + ) + if not referrer_is_maker: + remaining_accounts.append( + AccountMeta( + pubkey=referrer_info.referrer, is_writable=True, is_signer=False + ) + ) + remaining_accounts.append( + AccountMeta( + pubkey=referrer_info.referrer_stats, + is_writable=True, + is_signer=False, + ) + ) + + order_id = order.order_id + return self.program.instruction["fill_perp_order"]( + order_id, + None, + ctx=Context( + accounts={ + "state": self.get_state_public_key(), + "filler": filler_pubkey, + "filler_stats": filler_stats_pubkey, + "user": user_account_pubkey, + "user_stats": user_stats_pubkey, + "authority": self.authority, + }, + remaining_accounts=remaining_accounts, + ), + ) + + def get_revert_fill_ix(self): + filler_pubkey = self.get_user_account_public_key() + filler_stats_pubkey = self.get_user_stats_public_key() + + return self.program.instruction["revert_fill"]( + ctx=Context( + accounts={ + "state": self.get_state_public_key(), + "filler": filler_pubkey, + "filler_stats": filler_stats_pubkey, + "authority": self.authority, + } + ) + ) + + def get_trigger_order_ix( + self, + user_account_pubkey: Pubkey, + user_account: UserAccount, + order: Order, + filler_pubkey: Optional[Pubkey] = None, + ): + filler = filler_pubkey or self.get_user_account_public_key() + + if is_variant(order.market_type, "Perp"): + remaining_accounts = self.get_remaining_accounts( + user_accounts=[user_account], + writable_perp_market_indexes=[order.market_index], + ) + else: + remaining_accounts = self.get_remaining_accounts( + user_accounts=[user_account], + writable_spot_market_indexes=[ + order.market_index, + QUOTE_SPOT_MARKET_INDEX, + ], + ) + + return self.program.instruction["trigger_order"]( + order.order_id, + ctx=Context( + accounts={ + "state": self.get_state_public_key(), + "filler": filler, + "user": user_account_pubkey, + "authority": self.authority, + }, + remaining_accounts=remaining_accounts, + ), + ) + + async def force_cancel_orders( + self, + user_account_pubkey: Pubkey, + user_account: UserAccount, + filler_pubkey: Optional[Pubkey] = None, + ) -> Signature: + tx_sig_and_slot = await self.send_ixs( + self.get_force_cancel_orders_ix( + user_account_pubkey, user_account, filler_pubkey + ) + ) + + return tx_sig_and_slot.tx_sig + + def get_force_cancel_orders_ix( + self, + user_account_pubkey: Pubkey, + user_account: UserAccount, + filler_pubkey: Optional[Pubkey] = None, + ): + filler = filler_pubkey or self.get_user_account_public_key() + + remaining_accounts = self.get_remaining_accounts( + user_accounts=[user_account], + writable_spot_market_indexes=[QUOTE_SPOT_MARKET_INDEX], + ) + + return self.program.instruction["force_cancel_orders"]( + ctx=Context( + accounts={ + "state": self.get_state_public_key(), + "filler": filler, + "user": user_account_pubkey, + "authority": self.authority, + }, + remaining_accounts=remaining_accounts, + ) + ) + @deprecated async def open_position( self, diff --git a/src/driftpy/drift_user.py b/src/driftpy/drift_user.py index 7a8fdff3..2906be9e 100644 --- a/src/driftpy/drift_user.py +++ b/src/driftpy/drift_user.py @@ -957,9 +957,9 @@ def get_empty_position(self, market_index: int) -> PerpPosition: def get_perp_position_with_lp_settle( self, market_index: int, - originalPosition: PerpPosition = None, - burnLpShares: bool = False, - includeRemainderInBaseAmount: bool = False, + original_position: PerpPosition = None, + burn_lp_shares: bool = False, + include_remainder_in_base_amount: bool = False, ) -> Tuple[PerpPosition, int, int]: class UpdateType(Enum): OPEN = "open" @@ -968,139 +968,139 @@ class UpdateType(Enum): CLOSE = "close" FLIP = "flip" - originalPosition = ( - originalPosition + original_position = ( + original_position or self.get_perp_position(market_index) or self.get_empty_position(market_index) ) - if originalPosition.lp_shares == 0: - return originalPosition, 0, 0 + if original_position.lp_shares == 0: + return original_position, 0, 0 - position = copy.deepcopy(originalPosition) + position = copy.deepcopy(original_position) market = self.drift_client.get_perp_market_account(position.market_index) if market.amm.per_lp_base != position.per_lp_base: - expoDiff = market.amm.per_lp_base - position.per_lp_base - marketPerLpRebaseScalar = 10 ** abs(expoDiff) + expo_diff = market.amm.per_lp_base - position.per_lp_base + market_per_lp_rebase_scalar = 10 ** abs(expo_diff) - if expoDiff > 0: - position.lastbase_asset_amount_per_lp *= marketPerLpRebaseScalar - position.lastquote_asset_amount_per_lp *= marketPerLpRebaseScalar + if expo_diff > 0: + position.last_base_asset_amount_per_lp *= market_per_lp_rebase_scalar + position.last_quote_asset_amount_per_lp *= market_per_lp_rebase_scalar else: - position.lastbase_asset_amount_per_lp //= marketPerLpRebaseScalar - position.lastquote_asset_amount_per_lp //= marketPerLpRebaseScalar + position.last_base_asset_amount_per_lp //= market_per_lp_rebase_scalar + position.last_quote_asset_amount_per_lp //= market_per_lp_rebase_scalar - position.per_lp_base += expoDiff + position.per_lp_base += expo_diff - nShares = position.lp_shares + n_shares = position.lp_shares - quoteFundingPnl = calculate_position_funding_pnl(market, position) + quote_funding_pnl = calculate_position_funding_pnl(market, position) - baseUnit = int(AMM_RESERVE_PRECISION) + base_unit = int(AMM_RESERVE_PRECISION) if market.amm.per_lp_base == position.per_lp_base: if 0 <= position.per_lp_base <= 9: - marketPerLpRebase = 10**market.amm.per_lp_base - baseUnit *= marketPerLpRebase + market_per_lp_rebase = 10**market.amm.per_lp_base + base_unit *= market_per_lp_rebase elif position.per_lp_base < 0 and position.per_lp_base >= -9: - marketPerLpRebase = 10 ** abs(position.per_lp_base) - baseUnit //= marketPerLpRebase + market_per_lp_rebase = 10 ** abs(position.per_lp_base) + base_unit //= market_per_lp_rebase else: raise ValueError("cannot calc") else: - raise ValueError("market.amm.perLpBase != position.perLpBase") + raise ValueError("market.amm.per_lp_base != position.per_lp_base") - deltaBaa = ( + delta_baa = ( ( market.amm.base_asset_amount_per_lp - position.last_base_asset_amount_per_lp ) - * nShares - // baseUnit + * n_shares + // base_unit ) - deltaQaa = ( + delta_qaa = ( ( market.amm.quote_asset_amount_per_lp - position.last_quote_asset_amount_per_lp ) - * nShares - // baseUnit + * n_shares + // base_unit ) def sign(v: int) -> int: return -1 if v < 0 else 1 - def standardize(amount: int, stepSize: int) -> Tuple[int, int]: - remainder = abs(amount) % stepSize * sign(amount) - standardizedAmount = amount - remainder - return standardizedAmount, remainder + def standardize(amount: int, step_size: int) -> Tuple[int, int]: + remainder = abs(amount) % step_size * sign(amount) + standardized_amount = amount - remainder + return standardized_amount, remainder - standardizedBaa, remainderBaa = standardize( - deltaBaa, market.amm.order_step_size + standardized_baa, remainder_baa = standardize( + delta_baa, market.amm.order_step_size ) - position.remainder_base_asset_amount += remainderBaa + position.remainder_base_asset_amount += remainder_baa if abs(position.remainder_base_asset_amount) > market.amm.order_step_size: - newStandardizedBaa, newRemainderBaa = standardize( + new_standardized_baa, new_remainder_baa = standardize( position.remainder_base_asset_amount, market.amm.order_step_size ) - position.base_asset_amount += newStandardizedBaa - position.remainder_base_asset_amount = newRemainderBaa + position.base_asset_amount += new_standardized_baa + position.remainder_base_asset_amount = new_remainder_baa - dustBaseAssetValue = 0 - if burnLpShares and position.remainder_base_asset_amount != 0: - oraclePriceData = self.drift_client.get_oracle_data_for_perp_market( + dust_base_asset_value = 0 + if burn_lp_shares and position.remainder_base_asset_amount != 0: + oracle_price_data = self.drift_client.get_oracle_price_data_for_perp_market( position.market_index ) - dustBaseAssetValue = ( + dust_base_asset_value = ( abs(position.remainder_base_asset_amount) - * oraclePriceData.price + * oracle_price_data.price // AMM_RESERVE_PRECISION + 1 ) if position.base_asset_amount == 0: - updateType = UpdateType.OPEN - elif sign(position.base_asset_amount) == sign(deltaBaa): - updateType = UpdateType.INCREASE - elif abs(position.base_asset_amount) > abs(deltaBaa): - updateType = UpdateType.REDUCE - elif abs(position.base_asset_amount) == abs(deltaBaa): - updateType = UpdateType.CLOSE + update_type = UpdateType.OPEN + elif sign(position.base_asset_amount) == sign(delta_baa): + update_type = UpdateType.INCREASE + elif abs(position.base_asset_amount) > abs(delta_baa): + update_type = UpdateType.REDUCE + elif abs(position.base_asset_amount) == abs(delta_baa): + update_type = UpdateType.CLOSE else: - updateType = UpdateType.FLIP + update_type = UpdateType.FLIP - if updateType in [UpdateType.OPEN, UpdateType.INCREASE]: - newQuoteEntry = position.quote_entry_amount + deltaQaa + if update_type in [UpdateType.OPEN, UpdateType.INCREASE]: + new_quote_entry = position.quote_entry_amount + delta_qaa pnl = 0 - elif updateType in [UpdateType.REDUCE, UpdateType.CLOSE]: - newQuoteEntry = ( + elif update_type in [UpdateType.REDUCE, UpdateType.CLOSE]: + new_quote_entry = ( position.quote_entry_amount - position.quote_entry_amount - * abs(deltaBaa) + * abs(delta_baa) // abs(position.base_asset_amount) ) - pnl = position.quote_entry_amount - newQuoteEntry + deltaQaa + pnl = position.quote_entry_amount - new_quote_entry + delta_qaa else: - newQuoteEntry = deltaQaa - deltaQaa * abs( + new_quote_entry = delta_qaa - delta_qaa * abs( position.base_asset_amount - ) // abs(deltaBaa) - pnl = position.quote_entry_amount + deltaQaa - newQuoteEntry + ) // abs(delta_baa) + pnl = position.quote_entry_amount + delta_qaa - new_quote_entry - position.quote_entry_amount = newQuoteEntry - position.base_asset_amount += standardizedBaa + position.quote_entry_amount = new_quote_entry + position.base_asset_amount += standardized_baa position.quote_asset_amount = ( position.quote_asset_amount - + deltaQaa - + quoteFundingPnl - - dustBaseAssetValue + + delta_qaa + + quote_funding_pnl + - dust_base_asset_value ) position.quote_break_even_amount = ( position.quote_break_even_amount - + deltaQaa - + quoteFundingPnl - - dustBaseAssetValue + + delta_qaa + + quote_funding_pnl + - dust_base_asset_value ) market_open_bids, market_open_asks = calculate_market_open_bid_ask( @@ -1127,7 +1127,7 @@ def standardize(amount: int, stepSize: int) -> Tuple[int, int]: remainder_before_removal = position.remainder_base_asset_amount - if includeRemainderInBaseAmount: + if include_remainder_in_base_amount: position.base_asset_amount += remainder_before_removal position.remainder_base_asset_amount = 0 diff --git a/src/driftpy/math/amm.py b/src/driftpy/math/amm.py index 00cce8b2..35173ecf 100644 --- a/src/driftpy/math/amm.py +++ b/src/driftpy/math/amm.py @@ -473,7 +473,7 @@ def calculate_amm_reserves_after_swap( def get_swap_direction( input_asset_type: AssetType, position_direction: PositionDirection ) -> SwapDirection: - if is_variant(position_direction, "Long" and is_variant(input_asset_type, "BASE")): + if is_variant(position_direction, "Long") and is_variant(input_asset_type, "BASE"): return SwapDirection.Remove() if is_variant(position_direction, "Short") and is_variant( @@ -481,7 +481,7 @@ def get_swap_direction( ): return SwapDirection.Remove() - return SwapDirection.Add(0) + return SwapDirection.Add() def calculate_spread_reserves(amm: AMM, oracle_price_data: OraclePriceData, now=None): @@ -676,6 +676,9 @@ def calculate_max_base_asset_amount_to_trade( ((invariant * PRICE_PRECISION) * amm.peg_multiplier) // limit_price ) // PEG_PRECISION + if new_base_asset_reserve_squared < 0: + return (0, PositionDirection.Long()) + new_base_asset_reserve = math.sqrt(new_base_asset_reserve_squared) short_spread_reserves, long_spread_reserves = calculate_spread_reserves( diff --git a/src/driftpy/math/exchange_status.py b/src/driftpy/math/exchange_status.py index 14f1e6e1..b1c1ca2b 100644 --- a/src/driftpy/math/exchange_status.py +++ b/src/driftpy/math/exchange_status.py @@ -9,6 +9,17 @@ ) +class ExchangeStatusValues: + Active = 1 + DepositPaused = 2 + WithdrawPaused = 4 + AmmPaused = 8 + FillPaused = 16 + LiqPaused = 32 + FundingPaused = 64 + SettlePnlPaused = 128 + + def exchange_paused(state: StateAccount) -> bool: return not is_variant(state.exchange_status, "Active") @@ -17,8 +28,8 @@ def fill_paused( state: StateAccount, market: Union[PerpMarketAccount, SpotMarketAccount] ) -> bool: return ( - state.exchange_status & ExchangeStatus.FillPaused - ) == ExchangeStatus.FillPaused or is_one_of_variant( + state.exchange_status & ExchangeStatusValues.FillPaused + ) == ExchangeStatusValues.FillPaused or is_one_of_variant( market.status, ["Paused", "FillPaused"] ) @@ -27,7 +38,7 @@ def amm_paused( state: StateAccount, market: Union[PerpMarketAccount, SpotMarketAccount] ) -> bool: return ( - state.exchange_status & ExchangeStatus.AmmPaused - ) == ExchangeStatus.AmmPaused or is_one_of_variant( + state.exchange_status & ExchangeStatusValues.AmmPaused + ) == ExchangeStatusValues.AmmPaused or is_one_of_variant( market.status, ["Paused", "AmmPaused"] ) diff --git a/src/driftpy/math/funding.py b/src/driftpy/math/funding.py index d7ff92fc..b7ac3435 100644 --- a/src/driftpy/math/funding.py +++ b/src/driftpy/math/funding.py @@ -3,9 +3,11 @@ from typing import Optional, Tuple from driftpy.math.amm import calculate_bid_ask_price from driftpy.math.oracles import calculate_live_oracle_twap +from driftpy.math.utils import clamp_num from driftpy.types import ( OraclePriceData, PerpMarketAccount, + is_one_of_variant, is_variant, ) @@ -163,7 +165,15 @@ async def calculate_all_estimated_funding_rate( abs(oracle_twap) // FUNDING_RATE_OFFSET_DENOMINATOR ) - twap_spread_pct = (twap_spread_with_offset * PRICE_PRECISION * 100) // oracle_twap + max_spread = get_max_price_divergence_for_funding_rate(market, oracle_twap) + + clamped_spread_with_offset = clamp_num( + twap_spread_with_offset, (max_spread * -1), max_spread + ) + + twap_spread_pct = ( + clamped_spread_with_offset * PRICE_PRECISION * 100 + ) // oracle_twap seconds_in_hour = 3600 hours_in_day = 24 @@ -216,6 +226,17 @@ async def calculate_all_estimated_funding_rate( return mark_twap, oracle_twap, lowerbound_est, capped_alt_est, interp_est +def get_max_price_divergence_for_funding_rate( + market: PerpMarketAccount, oracle_twap: int +) -> int: + if is_one_of_variant(market.contract_tier, ["A", "B"]): + return oracle_twap // 33 + elif is_variant(market.contract_tier, "C"): + return oracle_twap // 20 + else: + return oracle_twap // 10 + + async def calculate_long_short_funding_and_live_twaps( market: PerpMarketAccount, oracle_price_data: OraclePriceData, diff --git a/src/driftpy/math/perp_position.py b/src/driftpy/math/perp_position.py index eeb16afa..e36d400e 100644 --- a/src/driftpy/math/perp_position.py +++ b/src/driftpy/math/perp_position.py @@ -142,7 +142,7 @@ def calculate_entry_price(market_position: PerpPosition): return 0 return abs( - market_position.quote_asset_amount + market_position.quote_entry_amount * PRICE_PRECISION * AMM_TO_QUOTE_PRECISION_RATIO / market_position.base_asset_amount diff --git a/src/driftpy/math/utils.py b/src/driftpy/math/utils.py index eaaff1de..e684d7be 100644 --- a/src/driftpy/math/utils.py +++ b/src/driftpy/math/utils.py @@ -1,2 +1,15 @@ def clamp_num(x: int, min_clamp: int, max_clamp: int) -> int: - return max(min_clamp, min(x, max_clamp)) \ No newline at end of file + return max(min_clamp, min(x, max_clamp)) + + +def div_ceil(a: int, b: int) -> int: + if b == 0: + return a + + quotient = a // b + remainder = a % b + + if remainder > 0: + return quotient + 1 + else: + return quotient diff --git a/src/driftpy/priority_fees/priority_fee_subscriber.py b/src/driftpy/priority_fees/priority_fee_subscriber.py index 78a25d96..45c4491b 100644 --- a/src/driftpy/priority_fees/priority_fee_subscriber.py +++ b/src/driftpy/priority_fees/priority_fee_subscriber.py @@ -52,7 +52,7 @@ async def load(self): headers={"content-encoding": "gzip"}, ) - resp = await asyncio.wait_for(post, timeout=5) + resp = await asyncio.wait_for(post, timeout=20) parsed_resp = jsonrpcclient.parse(resp.json()) diff --git a/src/driftpy/types.py b/src/driftpy/types.py index 64d8d28b..9f8628ce 100644 --- a/src/driftpy/types.py +++ b/src/driftpy/types.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field from solders.pubkey import Pubkey from borsh_construct.enum import _rust_enum from sumtypes import constructor @@ -239,8 +239,8 @@ class ContractTier: A = constructor() B = constructor() C = constructor() - SPECULATIVE = constructor() - ISOLATED = constructor() + Speculative = constructor() + Isolated = constructor() @_rust_enum @@ -347,11 +347,13 @@ class OrderParams: user_order_id: int = 0 price: int = 0 reduce_only: bool = False - post_only: PostOnlyParams = PostOnlyParams.NONE() + post_only: PostOnlyParams = field(default_factory=PostOnlyParams.NONE) immediate_or_cancel: bool = False max_ts: Optional[int] = None trigger_price: Optional[int] = None - trigger_condition: OrderTriggerCondition = OrderTriggerCondition.Above() + trigger_condition: OrderTriggerCondition = field( + default_factory=OrderTriggerCondition.Above + ) oracle_price_offset: Optional[int] = None auction_duration: Optional[int] = None auction_start_price: Optional[int] = None @@ -938,7 +940,7 @@ class OraclePriceData: confidence: int twap: int twap_confidence: int - has_sufficient_number_of_datapoints: bool + has_sufficient_number_of_data_points: bool @dataclass diff --git a/tests/amm.py b/tests/amm.py index 3f79d214..605cbb0e 100644 --- a/tests/amm.py +++ b/tests/amm.py @@ -33,7 +33,7 @@ async def test_orderbook_l2_gen_no_top_of_book_quote_amounts_10_num_orders_low_l confidence=1, twap=1, twap_confidence=1, - has_sufficient_number_of_datapoints=True, + has_sufficient_number_of_data_points=True, ) mock_1.amm.historical_oracle_data.last_oracle_price = int(18.5535 * PRICE_PRECISION) @@ -94,7 +94,7 @@ async def test_orderbook_l2_gen_no_top_of_book_quote_amounts_10_num_orders(): confidence=1, twap=1, twap_confidence=1, - has_sufficient_number_of_datapoints=True, + has_sufficient_number_of_data_points=True, ) mock_1.amm.historical_oracle_data.last_oracle_price = int(18.5535 * PRICE_PRECISION) @@ -147,7 +147,7 @@ async def test_orderbook_l2_gen_4_top_of_book_quote_amounts_10_num_orders(): confidence=1, twap=1, twap_confidence=1, - has_sufficient_number_of_datapoints=True, + has_sufficient_number_of_data_points=True, ) mock_market1.amm.historical_oracle_data.last_oracle_price = int( 18.5535 * PRICE_PRECISION @@ -213,7 +213,7 @@ async def test_orderbook_l2_gen_4_top_quote_amounts_10_orders_low_bid_liquidity( confidence=1, twap=1, twap_confidence=1, - has_sufficient_number_of_datapoints=True, + has_sufficient_number_of_data_points=True, ) mock_market1.amm.historical_oracle_data.last_oracle_price = int( 18.5535 * PRICE_PRECISION @@ -281,7 +281,7 @@ async def test_orderbook_l2_gen_4_top_quote_amounts_10_orders_low_ask_liquidity( confidence=1, twap=1, twap_confidence=1, - has_sufficient_number_of_datapoints=True, + has_sufficient_number_of_data_points=True, ) mock_market1.amm.historical_oracle_data.last_oracle_price = int( 18.5535 * PRICE_PRECISION @@ -349,7 +349,7 @@ async def test_orderbook_l2_gen_no_top_of_book_quote_amounts_10_orders_no_liquid confidence=1, twap=1, twap_confidence=1, - has_sufficient_number_of_datapoints=True, + has_sufficient_number_of_data_points=True, ) mock_market1.amm.historical_oracle_data.last_oracle_price = int( 18.5535 * PRICE_PRECISION