Skip to content

Commit 5a6053b

Browse files
authored
Merge pull request #32 from drift-labs:goldhaxx/DPE-3175/build-dashboard-for-high-leverage-user-spots-and-status
Update High Leverage API and Streamlit Page Enhancements
2 parents 3b770bf + 1659fa9 commit 5a6053b

File tree

2 files changed

+226
-50
lines changed

2 files changed

+226
-50
lines changed

backend/api/high_leverage_api.py

Lines changed: 125 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from driftpy.drift_user import DriftUser
1111
from driftpy.user_map.user_map import UserMap # Assuming UserMap is accessible
1212
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
13+
from driftpy.constants.numeric_constants import PRICE_PRECISION, MARGIN_PRECISION, BASE_PRECISION, QUOTE_PRECISION # Added QUOTE_PRECISION
1414
from driftpy.constants.perp_markets import mainnet_perp_market_configs # Added mainnet_perp_market_configs
1515
from driftpy.pickle.vat import Vat # Added Vat for type hinting
1616
from driftpy.math.margin import MarginCategory # Added MarginCategory
@@ -24,6 +24,10 @@
2424

2525
# Decimals for perp market base asset amount
2626
PERP_DECIMALS = 9
27+
# Slot inactivity threshold for considering a user bootable (approx 10 minutes)
28+
SLOT_INACTIVITY_THRESHOLD = 9000
29+
# Optional: Leverage threshold for booting (e.g., 25x)
30+
# BOOT_LEVERAGE_THRESHOLD = 25 # Not strictly implementing this yet, focusing on inactivity
2731

2832
class HighLeverageStats(BaseModel):
2933
slot: int # Added slot field
@@ -42,38 +46,44 @@ class HighLeveragePositionDetail(BaseModel):
4246
account_leverage: float # Renamed from leverage
4347
position_leverage: float # Added position-specific leverage
4448

49+
class BootableUserDetails(BaseModel):
50+
user_public_key: str
51+
authority: str
52+
account_leverage: float
53+
activity_staleness_slots: int
54+
last_active_slot: int
55+
initial_margin_requirement_usd: float
56+
total_collateral_usd: float
57+
health_percent: int # User's health percentage
58+
4559
@router.get("/stats", response_model=HighLeverageStats)
4660
async def get_high_leverage_stats(request: BackendRequest):
4761
"""
4862
Provides statistics about high leverage usage on the Drift protocol.
4963
- Total spots: Maximum users allowed in high leverage mode (hardcoded).
5064
- Opted-in spots: Users currently opted into high leverage mode.
5165
- 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).
66+
- Bootable spots: Users opted-in, inactive for a defined period, and potentially with low overall leverage.
5367
"""
5468

55-
total_spots = 200 # Hardcoded as per user request
69+
total_spots = 400 # Hardcoded maximum number of spots
5670

5771
opted_in_users_count = 0
5872
bootable_count = 0
5973

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 ---
74+
current_slot = getattr(request.state.backend_state, 'last_oracle_slot', 0)
75+
if current_slot == 0:
76+
logger.warning("Could not retrieve current_slot (last_oracle_slot) from backend state for /stats. Bootable check might be inaccurate.")
77+
# If current_slot is critical for bootable check, might return error or default.
78+
# For now, proceeding will mean inactivity check can't be reliably performed if current_slot is 0.
79+
6980
user_map: Optional[UserMap] = getattr(request.state.backend_state, 'user_map', None)
7081
logger.info(f"UserMap object type from state: {type(user_map)}")
7182

7283
if not user_map or not hasattr(user_map, 'values'):
7384
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)
7585
return HighLeverageStats(
76-
slot=slot,
86+
slot=current_slot,
7787
total_spots=total_spots,
7888
available_spots=total_spots,
7989
bootable_spots=0,
@@ -86,13 +96,13 @@ async def get_high_leverage_stats(request: BackendRequest):
8696

8797
if not user_values:
8898
logger.info("UserMap is empty for /stats.")
89-
else:
90-
logger.info(f"First user object type for /stats: {type(user_values[0])}")
99+
# else: # Logging first user type can be verbose, let's assume it's DriftUser by now
100+
# logger.info(f"First user object type for /stats: {type(user_values[0])}")
91101

92102
except Exception as e:
93103
logger.error(f"Error getting users from UserMap for /stats: {e}", exc_info=True)
94104
return HighLeverageStats(
95-
slot=slot,
105+
slot=current_slot,
96106
total_spots=total_spots,
97107
available_spots=total_spots,
98108
bootable_spots=0,
@@ -105,37 +115,47 @@ async def get_high_leverage_stats(request: BackendRequest):
105115
continue
106116

107117
is_high_leverage = False
118+
user_account: Optional[UserAccount] = None # Define here for broader scope
108119
try:
109120
is_high_leverage = user.is_high_leverage_mode()
121+
if is_high_leverage:
122+
user_account = user.get_user_account()
110123
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)
124+
logger.error(f"Error checking high leverage status or getting account for user {user.user_public_key} in /stats: {e}", exc_info=True)
112125
continue
113126

114-
if is_high_leverage:
127+
if is_high_leverage and user_account:
115128
opted_in_users_count += 1
116129

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
130+
# Check for bootable status based on inactivity
131+
is_inactive = False
132+
if current_slot > 0: # Ensure current_slot is valid before checking inactivity
133+
try:
134+
last_active_slot = user_account.last_active_slot # This is a int/BN
135+
# Ensure last_active_slot can be converted to int if it's a BN or similar type
136+
last_active_slot_int = int(str(last_active_slot))
137+
if (current_slot - last_active_slot_int) > SLOT_INACTIVITY_THRESHOLD:
138+
is_inactive = True
139+
logger.debug(f"User {user.user_public_key} is inactive. Current: {current_slot}, Last Active: {last_active_slot_int}, Diff: {current_slot - last_active_slot_int}")
140+
except Exception as slot_check_e:
141+
logger.error(f"Error checking inactivity for user {user.user_public_key}: {slot_check_e}", exc_info=True)
142+
# Decide behavior: treat as not inactive, or skip bootable check for this user
129143

130-
if not has_open_perp_positions:
144+
# The bot script uses inactivity and a general low leverage threshold.
145+
# For simplicity and alignment with bot, we use inactivity as the primary signal.
146+
# A stricter check could verify no *significant* positions or overall low leverage.
147+
if is_inactive:
148+
# Optionally, add the leverage check here if desired for stricter booting criteria:
149+
# current_leverage_ui = user.get_leverage() / MARGIN_PRECISION
150+
# if current_leverage_ui < BOOT_LEVERAGE_THRESHOLD:
151+
# bootable_count += 1
131152
bootable_count += 1
132153

133154
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}")
155+
logger.info(f"Calculated Stats for /stats: Slot={current_slot}, Total={total_spots}, OptedIn={opted_in_users_count}, Available={available_spots}, Bootable={bootable_count}")
135156

136-
# Include slot in the returned object
137157
return HighLeverageStats(
138-
slot=slot,
158+
slot=current_slot,
139159
total_spots=total_spots,
140160
available_spots=available_spots,
141161
bootable_spots=bootable_count,
@@ -252,8 +272,74 @@ async def get_high_leverage_positions_detailed(request: BackendRequest):
252272
logger.info(f"Returning {len(detailed_hl_positions)} high leverage positions.")
253273
return detailed_hl_positions
254274

275+
@router.get("/bootable-users", response_model=List[BootableUserDetails])
276+
async def get_bootable_user_details(request: BackendRequest):
277+
"""
278+
Returns detailed information for users who are in high leverage mode and deemed bootable due to inactivity.
279+
"""
280+
bootable_users_list: List[BootableUserDetails] = []
281+
282+
current_slot = getattr(request.state.backend_state, 'last_oracle_slot', 0)
283+
user_map: Optional[UserMap] = getattr(request.state.backend_state, 'user_map', None)
284+
285+
logger.info(f"Fetching bootable users details. Current slot: {current_slot}")
286+
287+
if current_slot == 0:
288+
logger.warning("Current slot is 0, cannot accurately determine bootable users by inactivity. Returning empty list.")
289+
return []
290+
291+
if not user_map or not hasattr(user_map, 'values'):
292+
logger.warning("UserMap not found or invalid in backend state. Returning empty list for /bootable-users.")
293+
return []
294+
295+
try:
296+
user_values = list(user_map.values())
297+
logger.info(f"Processing {len(user_values)} users from UserMap for /bootable-users.")
298+
except Exception as e:
299+
logger.error(f"Error getting users from UserMap for /bootable-users: {e}", exc_info=True)
300+
return []
301+
302+
for user in user_values:
303+
if not isinstance(user, DriftUser):
304+
logger.warning(f"Skipping item, expected DriftUser, got {type(user)}")
305+
continue
306+
307+
user_account: Optional[UserAccount] = None
308+
try:
309+
if user.is_high_leverage_mode():
310+
user_account = user.get_user_account()
311+
if not user_account:
312+
logger.warning(f"User {user.user_public_key} is high leverage but failed to get user_account. Skipping.")
313+
continue
314+
315+
last_active_slot_int = int(str(user_account.last_active_slot))
316+
activity_staleness_slots = current_slot - last_active_slot_int
317+
318+
if activity_staleness_slots > SLOT_INACTIVITY_THRESHOLD:
319+
logger.debug(f"User {user.user_public_key} is bootable. Staleness: {activity_staleness_slots} slots.")
320+
321+
account_leverage_ui = user.get_leverage() / MARGIN_PRECISION
322+
initial_margin_req_usd = user.get_margin_requirement(MarginCategory.INITIAL) / QUOTE_PRECISION
323+
total_collateral_usd = user.get_total_collateral(MarginCategory.INITIAL) / QUOTE_PRECISION
324+
health_percent = user.get_health()
325+
user_public_key_str = str(user.user_public_key)
326+
authority_str = str(user_account.authority)
255327

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"])
328+
bootable_users_list.append(
329+
BootableUserDetails(
330+
user_public_key=user_public_key_str,
331+
authority=authority_str,
332+
account_leverage=account_leverage_ui,
333+
activity_staleness_slots=activity_staleness_slots,
334+
last_active_slot=last_active_slot_int,
335+
initial_margin_requirement_usd=initial_margin_req_usd,
336+
total_collateral_usd=total_collateral_usd,
337+
health_percent=health_percent,
338+
)
339+
)
340+
except Exception as user_proc_e:
341+
logger.error(f"Error processing user {getattr(user, 'user_public_key', 'UNKNOWN')} for /bootable-users: {user_proc_e}", exc_info=True)
342+
continue
343+
344+
logger.info(f"Found {len(bootable_users_list)} bootable users.")
345+
return bootable_users_list

0 commit comments

Comments
 (0)