diff --git a/backend/api/__init__.py b/backend/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/api/asset_liability.py b/backend/api/asset_liability.py new file mode 100644 index 0000000..adcd33a --- /dev/null +++ b/backend/api/asset_liability.py @@ -0,0 +1,26 @@ +from backend.state import BackendRequest +from backend.state import BackendState +from backend.utils.matrix import get_matrix +from backend.utils.user_metrics import get_usermap_df +from driftpy.drift_client import DriftClient +from driftpy.pickle.vat import Vat +from fastapi import APIRouter + + +router = APIRouter() + + +@router.get("/matrix/{mode}/{perp_market_index}") +async def get_asset_liability_matrix( + request: BackendRequest, mode: int, perp_market_index: int +): + backend_state: BackendState = request.state.backend_state + vat: Vat = backend_state.vat + drift_client: DriftClient = backend_state.dc + + res, df = await get_matrix(drift_client, vat, mode, perp_market_index) + + return { + "res": res.to_dict(), + "df": df.to_dict(), + } diff --git a/backend/api/health.py b/backend/api/health.py new file mode 100644 index 0000000..e467aba --- /dev/null +++ b/backend/api/health.py @@ -0,0 +1,356 @@ +import heapq + +from backend.state import BackendRequest +from driftpy.constants import BASE_PRECISION +from driftpy.constants import PRICE_PRECISION +from driftpy.constants import SPOT_BALANCE_PRECISION +from driftpy.pickle.vat import Vat +from driftpy.types import is_variant +from fastapi import APIRouter +import pandas as pd + + +router = APIRouter() + + +def to_financial(num: float): + """ + Helper function to format a number to a financial format. + """ + num_str = str(num) + decimal_pos = num_str.find(".") + if decimal_pos != -1: + return float(num_str[: decimal_pos + 3]) + return num + + +@router.get("/health_distribution") +def get_account_health_distribution(request: BackendRequest): + """ + Get the distribution of account health across different ranges. + + This endpoint calculates the distribution of account health for all users, + categorizing them into health ranges and summing up the total collateral + in each range. + + Returns: + list[dict]: A list of dictionaries containing the health distribution data. + Each dictionary has the following keys: + - Health Range (str): The health percentage range (e.g., '0-10%') + - Counts (int): The number of accounts in this range + - Notional Values (float): The total collateral value in this range + """ + vat: Vat = request.state.backend_state.vat + health_notional_distributions = { + "0-10%": 0, + "10-20%": 0, + "20-30%": 0, + "30-40%": 0, + "40-50%": 0, + "50-60%": 0, + "60-70%": 0, + "70-80%": 0, + "80-90%": 0, + "90-100%": 0, + } + health_counts = { + "0-10%": 0, + "10-20%": 0, + "20-30%": 0, + "30-40%": 0, + "40-50%": 0, + "50-60%": 0, + "60-70%": 0, + "70-80%": 0, + "80-90%": 0, + "90-100%": 0, + } + + for user in vat.users.values(): + total_collateral = user.get_total_collateral() / PRICE_PRECISION + current_health = user.get_health() + match current_health: + case _ if current_health < 10: + health_notional_distributions["0-10%"] += total_collateral + health_counts["0-10%"] += 1 + case _ if current_health < 20: + health_notional_distributions["10-20%"] += total_collateral + health_counts["10-20%"] += 1 + case _ if current_health < 30: + health_notional_distributions["20-30%"] += total_collateral + health_counts["20-30%"] += 1 + case _ if current_health < 40: + health_notional_distributions["30-40%"] += total_collateral + health_counts["30-40%"] += 1 + case _ if current_health < 50: + health_notional_distributions["40-50%"] += total_collateral + health_counts["40-50%"] += 1 + case _ if current_health < 60: + health_notional_distributions["50-60%"] += total_collateral + health_counts["50-60%"] += 1 + case _ if current_health < 70: + health_notional_distributions["60-70%"] += total_collateral + health_counts["60-70%"] += 1 + case _ if current_health < 80: + health_notional_distributions["70-80%"] += total_collateral + health_counts["70-80%"] += 1 + case _ if current_health < 90: + health_notional_distributions["80-90%"] += total_collateral + health_counts["80-90%"] += 1 + case _: + health_notional_distributions["90-100%"] += total_collateral + health_counts["90-100%"] += 1 + df = pd.DataFrame( + { + "Health Range": list(health_counts.keys()), + "Counts": list(health_counts.values()), + "Notional Values": list(health_notional_distributions.values()), + } + ) + + return df.to_dict(orient="records") + + +@router.get("/largest_perp_positions") +def get_largest_perp_positions(request: BackendRequest): + """ + Get the top 10 largest perpetual positions by value. + + This endpoint retrieves the largest perpetual positions across all users, + calculated based on the current market prices. + + Returns: + dict: A dictionary containing lists of data for the top 10 positions: + - Market Index (list[int]): The market indices of the top positions + - Value (list[str]): The formatted dollar values of the positions + - Base Asset Amount (list[str]): The formatted base asset amounts + - Public Key (list[str]): The public keys of the position holders + """ + vat: Vat = request.state.backend_state.vat + top_positions: list[tuple[float, str, int, float]] = [] + + for user in vat.users.values(): + for position in user.get_user_account().perp_positions: + if position.base_asset_amount > 0: + market_price = vat.perp_oracles.get(position.market_index) + if market_price is not None: + market_price_ui = market_price.price / PRICE_PRECISION + base_asset_value = ( + abs(position.base_asset_amount) / BASE_PRECISION + ) * market_price_ui + heap_item = ( + to_financial(base_asset_value), + user.user_public_key, + position.market_index, + position.base_asset_amount / BASE_PRECISION, + ) + + if len(top_positions) < 10: + heapq.heappush(top_positions, heap_item) + else: + heapq.heappushpop(top_positions, heap_item) + + positions = sorted( + (value, pubkey, market_idx, amt) + for value, pubkey, market_idx, amt in top_positions + ) + + positions.reverse() + + data = { + "Market Index": [pos[2] for pos in positions], + "Value": [f"${pos[0]:,.2f}" for pos in positions], + "Base Asset Amount": [f"{pos[3]:,.2f}" for pos in positions], + "Public Key": [pos[1] for pos in positions], + } + + return data + + +@router.get("/most_levered_perp_positions_above_1m") +def get_most_levered_perp_positions_above_1m(request: BackendRequest): + """ + Get the top 10 most leveraged perpetual positions with value above $1 million. + + This endpoint calculates the leverage of each perpetual position with a value + over $1 million and returns the top 10 most leveraged positions. + + Returns: + dict: A dictionary containing lists of data for the top 10 leveraged positions: + - Market Index (list[int]): The market indices of the top positions + - Value (list[str]): The formatted dollar values of the positions + - Base Asset Amount (list[str]): The formatted base asset amounts + - Leverage (list[str]): The formatted leverage ratios + - Public Key (list[str]): The public keys of the position holders + """ + vat: Vat = request.state.backend_state.vat + top_positions: list[tuple[float, str, int, float, float]] = [] + + for user in vat.users.values(): + total_collateral = user.get_total_collateral() / PRICE_PRECISION + if total_collateral > 0: + for position in user.get_user_account().perp_positions: + if position.base_asset_amount > 0: + market_price = vat.perp_oracles.get(position.market_index) + if market_price is not None: + market_price_ui = market_price.price / PRICE_PRECISION + base_asset_value = ( + abs(position.base_asset_amount) / BASE_PRECISION + ) * market_price_ui + leverage = base_asset_value / total_collateral + if base_asset_value > 1_000_000: + heap_item = ( + to_financial(base_asset_value), + user.user_public_key, + position.market_index, + position.base_asset_amount / BASE_PRECISION, + leverage, + ) + + if len(top_positions) < 10: + heapq.heappush(top_positions, heap_item) + else: + heapq.heappushpop(top_positions, heap_item) + + positions = sorted( + top_positions, # We can sort directly the heap result + key=lambda x: x[ + 4 + ], # Sort by leverage, which is the fifth element in your tuple + ) + + positions.reverse() + + data = { + "Market Index": [pos[2] for pos in positions], + "Value": [f"${pos[0]:,.2f}" for pos in positions], + "Base Asset Amount": [f"{pos[3]:,.2f}" for pos in positions], + "Leverage": [f"{pos[4]:,.2f}" for pos in positions], + "Public Key": [pos[1] for pos in positions], + } + + return data + + +@router.get("/largest_spot_borrows") +def get_largest_spot_borrows(request: BackendRequest): + """ + Get the top 10 largest spot borrowing positions by value. + + This endpoint retrieves the largest spot borrowing positions across all users, + calculated based on the current market prices. + + Returns: + dict: A dictionary containing lists of data for the top 10 borrowing positions: + - Market Index (list[int]): The market indices of the top borrows + - Value (list[str]): The formatted dollar values of the borrows + - Scaled Balance (list[str]): The formatted scaled balances of the borrows + - Public Key (list[str]): The public keys of the borrowers + """ + vat: Vat = request.state.backend_state.vat + top_borrows: list[tuple[float, str, int, float]] = [] + + for user in vat.users.values(): + for position in user.get_user_account().spot_positions: + if position.scaled_balance > 0 and is_variant( + position.balance_type, "Borrow" + ): + market_price = vat.spot_oracles.get(position.market_index) + if market_price is not None: + market_price_ui = market_price.price / PRICE_PRECISION + borrow_value = ( + position.scaled_balance / SPOT_BALANCE_PRECISION + ) * market_price_ui + heap_item = ( + to_financial(borrow_value), + user.user_public_key, + position.market_index, + position.scaled_balance / SPOT_BALANCE_PRECISION, + ) + + if len(top_borrows) < 10: + heapq.heappush(top_borrows, heap_item) + else: + heapq.heappushpop(top_borrows, heap_item) + + borrows = sorted( + (value, pubkey, market_idx, amt) + for value, pubkey, market_idx, amt in top_borrows + ) + + borrows.reverse() + + data = { + "Market Index": [pos[2] for pos in borrows], + "Value": [f"${pos[0]:,.2f}" for pos in borrows], + "Scaled Balance": [f"{pos[3]:,.2f}" for pos in borrows], + "Public Key": [pos[1] for pos in borrows], + } + + return data + + +@router.get("/most_levered_spot_borrows_above_1m") +def get_most_levered_spot_borrows_above_1m(request: BackendRequest): + """ + Get the top 10 most leveraged spot borrowing positions with value above $750,000. + + This endpoint calculates the leverage of each spot borrowing position with a value + over $750,000 and returns the top 10 most leveraged positions. + + Returns: + dict: A dictionary containing lists of data for the top 10 leveraged borrowing positions: + - Market Index (list[int]): The market indices of the top borrows + - Value (list[str]): The formatted dollar values of the borrows + - Scaled Balance (list[str]): The formatted scaled balances of the borrows + - Leverage (list[str]): The formatted leverage ratios + - Public Key (list[str]): The public keys of the borrowers + """ + vat: Vat = request.state.backend_state.vat + top_borrows: list[tuple[float, str, int, float, float]] = [] + + for user in vat.users.values(): + total_collateral = user.get_total_collateral() / PRICE_PRECISION + if total_collateral > 0: + for position in user.get_user_account().spot_positions: + if ( + is_variant(position.balance_type, "Borrow") + and position.scaled_balance > 0 + ): + market_price = vat.spot_oracles.get(position.market_index) + if market_price is not None: + market_price_ui = market_price.price / PRICE_PRECISION + borrow_value = ( + position.scaled_balance / SPOT_BALANCE_PRECISION + ) * market_price_ui + leverage = borrow_value / total_collateral + if borrow_value > 750_000: + heap_item = ( + to_financial(borrow_value), + user.user_public_key, + position.market_index, + position.scaled_balance / SPOT_BALANCE_PRECISION, + leverage, + ) + + if len(top_borrows) < 10: + heapq.heappush(top_borrows, heap_item) + else: + heapq.heappushpop(top_borrows, heap_item) + + borrows = sorted( + top_borrows, + key=lambda x: x[4], + ) + + borrows.reverse() + + data = { + "Market Index": [pos[2] for pos in borrows], + "Value": [f"${pos[0]:,.2f}" for pos in borrows], + "Scaled Balance": [f"{pos[3]:,.2f}" for pos in borrows], + "Leverage": [f"{pos[4]:,.2f}" for pos in borrows], + "Public Key": [pos[1] for pos in borrows], + } + + return data diff --git a/backend/api/liquidation.py b/backend/api/liquidation.py new file mode 100644 index 0000000..a592268 --- /dev/null +++ b/backend/api/liquidation.py @@ -0,0 +1,45 @@ +from backend.state import BackendRequest +from driftpy.constants import BASE_PRECISION +from driftpy.constants import PRICE_PRECISION +from driftpy.pickle.vat import Vat +from fastapi import APIRouter + + +router = APIRouter() + + +@router.get("/liquidation-curve/{market_index}") +def get_liquidation_curve(request: BackendRequest, market_index: int): + vat: Vat = request.state.backend_state.vat + liquidations_long: list[tuple[float, float]] = [] + liquidations_short: list[tuple[float, float]] = [] + market_price = vat.perp_oracles.get(market_index) + market_price_ui = market_price.price / PRICE_PRECISION + for user in vat.users.user_map.values(): + perp_position = user.get_perp_position(market_index) + if perp_position is not None: + liquidation_price = user.get_perp_liq_price(market_index) + if liquidation_price is not None: + liquidation_price_ui = liquidation_price / PRICE_PRECISION + position_size = abs(perp_position.base_asset_amount) / BASE_PRECISION + position_notional = position_size * market_price_ui + is_zero = round(position_notional) == 0 + is_short = perp_position.base_asset_amount < 0 + is_long = perp_position.base_asset_amount > 0 + if is_zero: + continue + if is_short and liquidation_price_ui > market_price_ui: + liquidations_short.append((liquidation_price_ui, position_notional)) + elif is_long and liquidation_price_ui < market_price_ui: + liquidations_long.append((liquidation_price_ui, position_notional)) + else: + pass + + liquidations_long.sort(key=lambda x: x[0]) + liquidations_short.sort(key=lambda x: x[0]) + + return { + "liquidations_long": liquidations_long, + "liquidations_short": liquidations_short, + "market_price_ui": market_price_ui, + } diff --git a/backend/api/metadata.py b/backend/api/metadata.py new file mode 100644 index 0000000..d9621aa --- /dev/null +++ b/backend/api/metadata.py @@ -0,0 +1,15 @@ +from backend.state import BackendRequest +from backend.state import BackendState +from fastapi import APIRouter + + +router = APIRouter() + + +@router.get("/") +def get_metadata(request: BackendRequest): + backend_state: BackendState = request.state.backend_state + return { + "pickle_file": backend_state.current_pickle_path, + "last_oracle_slot": backend_state.vat.register_oracle_slot, + } diff --git a/backend/api/price_shock.py b/backend/api/price_shock.py new file mode 100644 index 0000000..0b49a99 --- /dev/null +++ b/backend/api/price_shock.py @@ -0,0 +1,36 @@ +from typing import Optional + +from backend.state import BackendRequest +from backend.state import BackendState +from backend.utils.matrix import get_usermap_df +from driftpy.drift_client import DriftClient +from driftpy.pickle.vat import Vat +from fastapi import APIRouter + + +router = APIRouter() + + +@router.get("/usermap") +async def get_price_shock( + request: BackendRequest, + oracle_distortion: float = 0.1, + asset_group: Optional[str] = None, + n_scenarios: int = 5, +): + backend_state: BackendState = request.state.backend_state + vat: Vat = backend_state.vat + drift_client: DriftClient = backend_state.dc + + result = await get_usermap_df( + drift_client, + vat.users, + "oracles", + oracle_distortion, + asset_group, + n_scenarios, + ) + + print(result) + + return {}