Skip to content

Commit 3b770bf

Browse files
authored
Merge pull request #31 from drift-labs:goldhaxx/DPE-3175/build-dashboard-for-high-leverage-user-spots-and-status
Add High Leverage API and Streamlit Page Integration
2 parents 26429b6 + 56a507f commit 3b770bf

File tree

4 files changed

+418
-0
lines changed

4 files changed

+418
-0
lines changed

backend/api/high_leverage_api.py

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
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"])

backend/app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
snapshot,
2424
ucache,
2525
vaults,
26+
high_leverage_api,
2627
)
2728
from backend.middleware.cache_middleware import CacheMiddleware
2829
from backend.middleware.readiness import ReadinessMiddleware
@@ -90,6 +91,7 @@ async def lifespan(app: FastAPI):
9091
app.include_router(positions.router, prefix="/api/positions", tags=["positions"])
9192
app.include_router(market_recommender_api.router, prefix="/api/market-recommender", tags=["market-recommender"])
9293
app.include_router(open_interest_api.router, prefix="/api/open-interest", tags=["open-interest"])
94+
app.include_router(high_leverage_api.router, prefix="/api/high-leverage", tags=["high-leverage"])
9395
# NOTE: All other routes should be in /api/* within the /api folder. Routes outside of /api are not exposed in k8s
9496
@app.get("/")
9597
async def root():

src/main.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from page.welcome import welcome_page
2323
from page.market_recommender_page import market_recommender_page
2424
from page.open_interest_page import open_interest_page
25+
from page.high_leverage_page import high_leverage_page
2526

2627
load_dotenv()
2728

@@ -142,6 +143,12 @@ def apply_custom_css(css):
142143
title="Open Interest",
143144
icon="💰",
144145
),
146+
st.Page(
147+
high_leverage_page,
148+
url_path="high-leverage",
149+
title="High Leverage",
150+
icon="⚡",
151+
),
145152
]
146153

147154
pg = st.navigation(

0 commit comments

Comments
 (0)