Skip to content

Commit 1659fa9

Browse files
committed
Enhance High Leverage API and Streamlit Page with Bootable User Details
- Added a new endpoint `/bootable-users` to the High Leverage API, providing detailed information about users in high leverage mode who are deemed bootable due to inactivity. - Introduced a new `BootableUserDetails` model to encapsulate user data for bootable users, including fields such as account leverage, activity staleness, and health percentage. - Updated the Streamlit page to display bootable user details, enhancing user experience by providing insights into inactive high leverage users. - Improved logging and error handling in both the API and Streamlit components to ensure robust data processing and user feedback. These changes significantly enhance the functionality and usability of the High Leverage features in the application, allowing for better management of user activity and leverage status.
1 parent 06fc071 commit 1659fa9

File tree

2 files changed

+210
-42
lines changed

2 files changed

+210
-42
lines changed

backend/api/high_leverage_api.py

Lines changed: 127 additions & 35 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

5569
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,
@@ -250,4 +270,76 @@ async def get_high_leverage_positions_detailed(request: BackendRequest):
250270
continue # Skip to next user on error
251271

252272
logger.info(f"Returning {len(detailed_hl_positions)} high leverage positions.")
253-
return detailed_hl_positions
273+
return detailed_hl_positions
274+
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)
327+
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

src/page/high_leverage_page.py

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -130,22 +130,22 @@ def high_leverage_page():
130130
cols[0].metric("Total High Leverage Spots", stats_data.get('total_spots', 'N/A'))
131131
cols[1].metric("Opted-In Users", stats_data.get('opted_in_spots', 'N/A'))
132132
cols[2].metric("Available Spots", stats_data.get('available_spots', 'N/A'))
133-
cols[3].metric("Bootable Spots (Opted-in, No Position)", stats_data.get('bootable_spots', 'N/A'))
133+
cols[3].metric("Bootable Spots (Inactive)", stats_data.get('bootable_spots', 'N/A')) # Updated label
134134

135135
st.subheader("Detailed High Leverage Positions")
136136
if not display_df.empty:
137137
# Apply Pandas Styler for formatting
138138
styled_df = display_df.style.format({
139-
'Base Asset Amount': '{:,.4f}', # Commas, 4 decimal places
140-
'Notional Value (USD)': '${:,.2f}', # Commas, 2 decimal places, no $
141-
'Position Leverage': '{:.2f}x', # 2 decimal places, 'x' suffix
142-
'Account Leverage': '{:.2f}x' # 2 decimal places, 'x' suffix
139+
'Base Asset Amount': '{:,.4f}',
140+
'Notional Value (USD)': '${:,.2f}',
141+
'Position Leverage': '{:.2f}x',
142+
'Account Leverage': '{:.2f}x'
143143
})
144144
st.dataframe(
145-
styled_df, # Pass the styled DataFrame
145+
styled_df,
146146
hide_index=True,
147147
use_container_width=True,
148-
column_config={ # Keep config for non-styled columns or for relabeling if needed
148+
column_config={
149149
"Market Symbol": st.column_config.TextColumn(label="Market Symbol"),
150150
"User Account": st.column_config.TextColumn(label="User Account"),
151151
"Authority": st.column_config.TextColumn(label="Authority"),
@@ -155,6 +155,82 @@ def high_leverage_page():
155155
else:
156156
st.info("No detailed high leverage position data available, or an error occurred during processing.")
157157

158+
# --- 8. Fetch and Display Bootable Users Details ---
159+
st.subheader("Bootable User Details (Inactive High Leverage Users)")
160+
result_bootable_users = fetch_api_data(
161+
section="high-leverage",
162+
path="bootable-users",
163+
retry=False
164+
)
165+
166+
if is_processing(result_bootable_users):
167+
st.info(f"Backend is processing bootable user data. Auto-refreshing in {RETRY_DELAY_SECONDS} seconds...")
168+
# No need for a full page rerun here if other data is fine, but spinner helps UX
169+
with st.spinner("Loading bootable users..."):
170+
time.sleep(RETRY_DELAY_SECONDS)
171+
st.rerun() # Or trigger a more targeted refresh if possible
172+
return
173+
174+
if has_error(result_bootable_users):
175+
st.warning("Could not fetch Bootable User Details. This table will be empty.")
176+
bootable_users_data = []
177+
else:
178+
bootable_users_data = result_bootable_users if isinstance(result_bootable_users, list) else []
179+
180+
df_bootable = pd.DataFrame()
181+
if bootable_users_data:
182+
try:
183+
df_bootable = pd.DataFrame(bootable_users_data)
184+
if not df_bootable.empty:
185+
df_bootable.rename(columns={
186+
'user_public_key': 'User Account',
187+
'authority': 'Authority',
188+
'account_leverage': 'Account Leverage',
189+
'activity_staleness_slots': 'Activity Staleness (Slots)',
190+
'last_active_slot': 'Last Active Slot',
191+
'initial_margin_requirement_usd': 'Initial Margin Req. (USD)',
192+
'total_collateral_usd': 'Total Collateral (USD)',
193+
'health_percent': 'Health (%)'
194+
}, inplace=True)
195+
196+
# Ensure numeric types for sorting where applicable
197+
df_bootable['Account Leverage'] = pd.to_numeric(df_bootable['Account Leverage'], errors='coerce')
198+
df_bootable['Activity Staleness (Slots)'] = pd.to_numeric(df_bootable['Activity Staleness (Slots)'], errors='coerce')
199+
df_bootable['Initial Margin Req. (USD)'] = pd.to_numeric(df_bootable['Initial Margin Req. (USD)'], errors='coerce')
200+
df_bootable['Total Collateral (USD)'] = pd.to_numeric(df_bootable['Total Collateral (USD)'], errors='coerce')
201+
df_bootable['Health (%)'] = pd.to_numeric(df_bootable['Health (%)'], errors='coerce')
202+
203+
# Default sort by Activity Staleness (descending)
204+
df_bootable = df_bootable.sort_values(by='Activity Staleness (Slots)', ascending=False)
205+
206+
# Select and order columns for display
207+
display_bootable_df = df_bootable[[
208+
'User Account',
209+
'Authority',
210+
'Account Leverage',
211+
'Activity Staleness (Slots)',
212+
'Last Active Slot',
213+
'Initial Margin Req. (USD)',
214+
'Total Collateral (USD)',
215+
'Health (%)'
216+
]].copy()
217+
218+
# Display formatting using Pandas Styler
219+
styled_bootable_df = display_bootable_df.style.format({
220+
'Account Leverage': '{:.2f}x',
221+
'Activity Staleness (Slots)': '{:,.0f}',
222+
'Initial Margin Req. (USD)': '${:,.2f}',
223+
'Total Collateral (USD)': '${:,.2f}',
224+
'Health (%)': '{:.0f}%'
225+
})
226+
st.dataframe(styled_bootable_df, hide_index=True, use_container_width=True)
227+
else:
228+
st.info("No bootable users found meeting the criteria.") # Message if data is empty after processing
229+
except Exception as df_boot_e:
230+
st.error(f"Error processing bootable user data into DataFrame: {df_boot_e}")
231+
else:
232+
st.info("No bootable user data to display.")
233+
158234
except Exception as e:
159235
st.error(f"An error occurred while displaying the page: {e}")
160236
import traceback

0 commit comments

Comments
 (0)