|
| 1 | +import asyncio |
| 2 | +from typing import List, Optional # Added List |
| 3 | +from fastapi import APIRouter, Depends |
| 4 | +from pydantic import BaseModel |
| 5 | + |
| 6 | +# backend.state for BackendRequest |
| 7 | +from backend.state import BackendRequest |
| 8 | + |
| 9 | +# Drift imports - adjust paths/names if necessary based on project structure |
| 10 | +from driftpy.drift_user import DriftUser |
| 11 | +from driftpy.user_map.user_map import UserMap # Assuming UserMap is accessible |
| 12 | +from driftpy.types import PerpPosition, UserAccount, is_variant, OraclePriceData # Added OraclePriceData |
| 13 | +from driftpy.constants.numeric_constants import PRICE_PRECISION, MARGIN_PRECISION, BASE_PRECISION # Added BASE_PRECISION |
| 14 | +from driftpy.constants.perp_markets import mainnet_perp_market_configs # Added mainnet_perp_market_configs |
| 15 | +from driftpy.pickle.vat import Vat # Added Vat for type hinting |
| 16 | +from driftpy.math.margin import MarginCategory # Added MarginCategory |
| 17 | + |
| 18 | +# Add logging |
| 19 | +import logging |
| 20 | +logging.basicConfig(level=logging.INFO) |
| 21 | +logger = logging.getLogger(__name__) |
| 22 | + |
| 23 | +router = APIRouter() |
| 24 | + |
| 25 | +# Decimals for perp market base asset amount |
| 26 | +PERP_DECIMALS = 9 |
| 27 | + |
| 28 | +class HighLeverageStats(BaseModel): |
| 29 | + slot: int # Added slot field |
| 30 | + total_spots: int |
| 31 | + available_spots: int |
| 32 | + bootable_spots: int |
| 33 | + opted_in_spots: int |
| 34 | + |
| 35 | +class HighLeveragePositionDetail(BaseModel): |
| 36 | + user_public_key: str |
| 37 | + authority: str |
| 38 | + market_index: int |
| 39 | + market_symbol: str |
| 40 | + base_asset_amount_ui: float |
| 41 | + position_value_usd: float |
| 42 | + account_leverage: float # Renamed from leverage |
| 43 | + position_leverage: float # Added position-specific leverage |
| 44 | + |
| 45 | +@router.get("/stats", response_model=HighLeverageStats) |
| 46 | +async def get_high_leverage_stats(request: BackendRequest): |
| 47 | + """ |
| 48 | + Provides statistics about high leverage usage on the Drift protocol. |
| 49 | + - Total spots: Maximum users allowed in high leverage mode (hardcoded). |
| 50 | + - Opted-in spots: Users currently opted into high leverage mode. |
| 51 | + - Available spots: Spots remaining for users to opt-in. |
| 52 | + - Bootable spots: Users opted-in but not actively using high leverage (no significant perp positions). |
| 53 | + """ |
| 54 | + |
| 55 | + total_spots = 200 # Hardcoded as per user request |
| 56 | + |
| 57 | + opted_in_users_count = 0 |
| 58 | + bootable_count = 0 |
| 59 | + |
| 60 | + # --- Get Slot --- |
| 61 | + # Use getattr for safer access, default to 0 or handle appropriately if slot is critical |
| 62 | + slot = getattr(request.state.backend_state, 'last_oracle_slot', 0) |
| 63 | + if slot == 0: |
| 64 | + logger.warning("Could not retrieve last_oracle_slot from backend state.") |
| 65 | + # Decide how to handle missing slot: raise error, return default, etc. |
| 66 | + # For now, it will return 0 as the slot. |
| 67 | + |
| 68 | + # --- Access UserMap --- |
| 69 | + user_map: Optional[UserMap] = getattr(request.state.backend_state, 'user_map', None) |
| 70 | + logger.info(f"UserMap object type from state: {type(user_map)}") |
| 71 | + |
| 72 | + if not user_map or not hasattr(user_map, 'values'): |
| 73 | + logger.warning("UserMap not found or invalid in backend state. Returning default stats.") |
| 74 | + # Return default values, including the fetched slot (which might be 0) |
| 75 | + return HighLeverageStats( |
| 76 | + slot=slot, |
| 77 | + total_spots=total_spots, |
| 78 | + available_spots=total_spots, |
| 79 | + bootable_spots=0, |
| 80 | + opted_in_spots=0 |
| 81 | + ) |
| 82 | + |
| 83 | + try: |
| 84 | + user_values = list(user_map.values()) |
| 85 | + logger.info(f"Processing {len(user_values)} users from UserMap for /stats.") |
| 86 | + |
| 87 | + if not user_values: |
| 88 | + logger.info("UserMap is empty for /stats.") |
| 89 | + else: |
| 90 | + logger.info(f"First user object type for /stats: {type(user_values[0])}") |
| 91 | + |
| 92 | + except Exception as e: |
| 93 | + logger.error(f"Error getting users from UserMap for /stats: {e}", exc_info=True) |
| 94 | + return HighLeverageStats( |
| 95 | + slot=slot, |
| 96 | + total_spots=total_spots, |
| 97 | + available_spots=total_spots, |
| 98 | + bootable_spots=0, |
| 99 | + opted_in_spots=0 |
| 100 | + ) |
| 101 | + |
| 102 | + for user in user_values: |
| 103 | + if not isinstance(user, DriftUser): |
| 104 | + logger.warning(f"Skipping item in user_map values for /stats, expected DriftUser, got {type(user)}") |
| 105 | + continue |
| 106 | + |
| 107 | + is_high_leverage = False |
| 108 | + try: |
| 109 | + is_high_leverage = user.is_high_leverage_mode() |
| 110 | + except Exception as e: |
| 111 | + logger.error(f"Error checking high leverage status for user {user.user_public_key} in /stats: {e}", exc_info=True) |
| 112 | + continue |
| 113 | + |
| 114 | + if is_high_leverage: |
| 115 | + opted_in_users_count += 1 |
| 116 | + |
| 117 | + has_open_perp_positions = False |
| 118 | + try: |
| 119 | + user_account: UserAccount = user.get_user_account() |
| 120 | + perp_positions: list[PerpPosition] = user_account.perp_positions |
| 121 | + |
| 122 | + for position in perp_positions: |
| 123 | + if position.base_asset_amount != 0: |
| 124 | + has_open_perp_positions = True |
| 125 | + break |
| 126 | + except Exception as e: |
| 127 | + logger.error(f"Error checking positions for user {user.user_public_key} in /stats: {e}", exc_info=True) |
| 128 | + has_open_perp_positions = True |
| 129 | + |
| 130 | + if not has_open_perp_positions: |
| 131 | + bootable_count += 1 |
| 132 | + |
| 133 | + available_spots = total_spots - opted_in_users_count |
| 134 | + logger.info(f"Calculated Stats for /stats: Slot={slot}, Total={total_spots}, OptedIn={opted_in_users_count}, Available={available_spots}, Bootable={bootable_count}") |
| 135 | + |
| 136 | + # Include slot in the returned object |
| 137 | + return HighLeverageStats( |
| 138 | + slot=slot, |
| 139 | + total_spots=total_spots, |
| 140 | + available_spots=available_spots, |
| 141 | + bootable_spots=bootable_count, |
| 142 | + opted_in_spots=opted_in_users_count |
| 143 | + ) |
| 144 | + |
| 145 | +@router.get("/positions/detailed", response_model=List[HighLeveragePositionDetail]) |
| 146 | +async def get_high_leverage_positions_detailed(request: BackendRequest): |
| 147 | + """ |
| 148 | + Returns detailed information for all open perp positions held by users in high leverage mode, |
| 149 | + including the user's current account leverage and position-specific leverage. |
| 150 | + """ |
| 151 | + detailed_hl_positions: List[HighLeveragePositionDetail] = [] |
| 152 | + |
| 153 | + user_map: Optional[UserMap] = getattr(request.state.backend_state, 'user_map', None) |
| 154 | + vat: Optional[Vat] = getattr(request.state.backend_state, 'vat', None) |
| 155 | + |
| 156 | + logger.info(f"UserMap type for /positions/detailed: {type(user_map)}") |
| 157 | + logger.info(f"VAT type for /positions/detailed: {type(vat)}") |
| 158 | + |
| 159 | + if not user_map or not hasattr(user_map, 'values') or not vat or not hasattr(vat, 'perp_oracles'): |
| 160 | + logger.warning("UserMap or VAT (with perp_oracles) not found/invalid. Returning empty list for /positions/detailed.") |
| 161 | + return [] |
| 162 | + |
| 163 | + try: |
| 164 | + user_values = list(user_map.values()) |
| 165 | + logger.info(f"Processing {len(user_values)} users from UserMap for /positions/detailed.") |
| 166 | + except Exception as e: |
| 167 | + logger.error(f"Error getting users from UserMap for /positions/detailed: {e}", exc_info=True) |
| 168 | + return [] |
| 169 | + |
| 170 | + for user in user_values: |
| 171 | + if not isinstance(user, DriftUser): |
| 172 | + logger.warning(f"Skipping item in user_map values for /positions/detailed, expected DriftUser, got {type(user)}") |
| 173 | + continue |
| 174 | + |
| 175 | + try: |
| 176 | + if user.is_high_leverage_mode(): |
| 177 | + user_account: UserAccount = user.get_user_account() |
| 178 | + user_public_key_str = str(user.user_public_key) |
| 179 | + authority_str = str(user_account.authority) |
| 180 | + |
| 181 | + # Calculate user's account leverage |
| 182 | + account_leverage_raw = user.get_leverage() |
| 183 | + account_leverage_ui = account_leverage_raw / MARGIN_PRECISION |
| 184 | + logger.debug(f"User {user_public_key_str} is high leverage. Account Leverage: {account_leverage_ui:.2f}x (raw: {account_leverage_raw})") |
| 185 | + |
| 186 | + for position in user_account.perp_positions: |
| 187 | + if position.base_asset_amount == 0: |
| 188 | + continue |
| 189 | + |
| 190 | + market_index = position.market_index |
| 191 | + oracle_price_data: Optional[OraclePriceData] = vat.perp_oracles.get(market_index) |
| 192 | + |
| 193 | + if oracle_price_data is None: |
| 194 | + logger.warning(f"Missing oracle price data for market_index {market_index} (user {user_public_key_str}). Skipping position.") |
| 195 | + continue |
| 196 | + |
| 197 | + base_asset_amount_val = position.base_asset_amount |
| 198 | + if base_asset_amount_val is None: |
| 199 | + logger.warning(f"Position base_asset_amount is None for user {user_public_key_str}, market {market_index}. Skipping position.") |
| 200 | + continue |
| 201 | + |
| 202 | + try: |
| 203 | + # Calculate Base Value and Notional (UI) |
| 204 | + oracle_price = float(oracle_price_data.price) / PRICE_PRECISION |
| 205 | + base_asset_amount_ui = base_asset_amount_val / (10**PERP_DECIMALS) |
| 206 | + position_value_usd = abs(base_asset_amount_ui) * oracle_price |
| 207 | + |
| 208 | + # Calculate Position Leverage |
| 209 | + margin_requirement_raw = user.calculate_weighted_perp_position_liability( |
| 210 | + position, |
| 211 | + MarginCategory.INITIAL, |
| 212 | + 0, # liquidation_buffer |
| 213 | + True # include_open_orders |
| 214 | + ) |
| 215 | + |
| 216 | + base_asset_value_raw = abs(position.base_asset_amount) * oracle_price_data.price // BASE_PRECISION |
| 217 | + |
| 218 | + position_leverage_ui = 0.0 |
| 219 | + if margin_requirement_raw != 0: |
| 220 | + position_leverage_raw = (base_asset_value_raw * MARGIN_PRECISION) // margin_requirement_raw |
| 221 | + position_leverage_ui = position_leverage_raw / MARGIN_PRECISION |
| 222 | + else: |
| 223 | + # Handle case with zero margin requirement (e.g., negligible position size or specific market state) |
| 224 | + logger.debug(f"Margin requirement raw is 0 for user {user_public_key_str}, market {market_index}. Setting position leverage to 0.") |
| 225 | + |
| 226 | + # Get Market Symbol |
| 227 | + market_symbol = 'N/A' |
| 228 | + if market_index < len(mainnet_perp_market_configs): |
| 229 | + market_symbol = mainnet_perp_market_configs[market_index].symbol |
| 230 | + else: |
| 231 | + logger.warning(f"market_index {market_index} out of range for mainnet_perp_market_configs.") |
| 232 | + |
| 233 | + detailed_hl_positions.append( |
| 234 | + HighLeveragePositionDetail( |
| 235 | + user_public_key=user_public_key_str, |
| 236 | + authority=authority_str, |
| 237 | + market_index=market_index, |
| 238 | + market_symbol=market_symbol, |
| 239 | + base_asset_amount_ui=base_asset_amount_ui, |
| 240 | + position_value_usd=position_value_usd, |
| 241 | + account_leverage=account_leverage_ui, # Account leverage |
| 242 | + position_leverage=position_leverage_ui # Position leverage |
| 243 | + ) |
| 244 | + ) |
| 245 | + except (TypeError, ValueError, AttributeError) as calc_e: # Added AttributeError for safety |
| 246 | + logger.error(f"Error calculating position data or leverage for user {user_public_key_str}, market {market_index}: {calc_e}. Skipping.", exc_info=True) |
| 247 | + continue |
| 248 | + except Exception as user_proc_e: |
| 249 | + logger.error(f"Error processing user {getattr(user, 'user_public_key', 'UNKNOWN')} for /positions/detailed: {user_proc_e}", exc_info=True) |
| 250 | + continue # Skip to next user on error |
| 251 | + |
| 252 | + logger.info(f"Returning {len(detailed_hl_positions)} high leverage positions.") |
| 253 | + return detailed_hl_positions |
| 254 | + |
| 255 | + |
| 256 | +# Example of how to include this router in a main FastAPI app: |
| 257 | +# from fastapi import FastAPI |
| 258 | +# app = FastAPI() |
| 259 | +# app.include_router(router, prefix="/high-leverage", tags=["High Leverage"]) |
0 commit comments