Skip to content

Commit 26429b6

Browse files
authored
Merge pull request #30 from drift-labs:goldhaxx/DPE-3192/oi-statistics-per-authority-and-user-account
Enhance Open Interest API and Streamlit Page Functionality
2 parents 5b17beb + a8eafc4 commit 26429b6

File tree

2 files changed

+279
-33
lines changed

2 files changed

+279
-33
lines changed

backend/api/open_interest_api.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from fastapi import APIRouter
22
from driftpy.constants.numeric_constants import PRICE_PRECISION
3+
from driftpy.constants.config import mainnet_perp_market_configs
34

45
from backend.state import BackendRequest
56

@@ -65,6 +66,142 @@ async def _get_open_interest_per_authority(request: BackendRequest) -> dict:
6566
"data": result_list,
6667
}
6768

69+
async def _get_open_interest_per_account(request: BackendRequest) -> dict:
70+
vat = request.state.backend_state.vat
71+
slot = request.state.backend_state.last_oracle_slot
72+
73+
oi_per_account = {}
74+
75+
for user_data in vat.users.values(): # user_data is of type UserMapItem (based on health.py it should have user_public_key)
76+
user_account = user_data.get_user_account()
77+
78+
if user_account is None:
79+
# Optionally log this: print(f"Warning: Skipping user_data as get_user_account() returned None.")
80+
continue
81+
82+
user_public_key = str(user_data.user_public_key) # Get user_public_key from user_data
83+
authority = str(user_account.authority) # Get authority from user_account
84+
85+
current_oi_for_account = oi_per_account.get(user_public_key, {
86+
'total_open_interest_usd': 0.0,
87+
'user_public_key': user_public_key, # Store user_public_key in the dict
88+
'authority': authority # Add authority field
89+
})
90+
91+
for position in user_account.perp_positions:
92+
if position.base_asset_amount == 0:
93+
continue
94+
95+
market_index = position.market_index
96+
oracle_price_data = vat.perp_oracles.get(market_index)
97+
98+
if oracle_price_data is None:
99+
print(f"Warning: Missing oracle price data for market_index {market_index} for user {user_public_key}. Skipping position.")
100+
continue
101+
102+
try:
103+
oracle_price = float(oracle_price_data.price) / PRICE_PRECISION
104+
# All perpetual markets use BASE_PRECISION (10^9) for base asset amounts.
105+
decimals = 9
106+
107+
base_asset_amount_val = position.base_asset_amount
108+
if base_asset_amount_val is None: # Should not happen with base_asset_amount == 0 check, but good for safety
109+
print(f"Warning: Position base_asset_amount is None for user {user_public_key}, market {market_index}. Skipping position.")
110+
continue
111+
112+
position_value_usd = (abs(base_asset_amount_val) / (10**decimals)) * oracle_price
113+
current_oi_for_account['total_open_interest_usd'] += position_value_usd
114+
except (TypeError, ValueError) as e:
115+
base_val_repr = repr(getattr(position, 'base_asset_amount', 'N/A'))
116+
oracle_price_repr = repr(getattr(oracle_price_data, 'price', 'N/A'))
117+
print(f"Error calculating position_value_usd for user {user_public_key}, market {market_index}: {e}. Base: {base_val_repr}, OraclePriceRaw: {oracle_price_repr}. Skipping position.")
118+
continue
119+
120+
if current_oi_for_account['total_open_interest_usd'] > 0:
121+
oi_per_account[user_public_key] = current_oi_for_account
122+
123+
result_list = sorted(list(oi_per_account.values()), key=lambda x: x['total_open_interest_usd'], reverse=True)
124+
125+
return {
126+
"slot": slot,
127+
"data": result_list,
128+
}
129+
130+
async def _get_open_positions_detailed(request: BackendRequest) -> dict:
131+
vat = request.state.backend_state.vat
132+
slot = request.state.backend_state.last_oracle_slot
133+
134+
detailed_positions = []
135+
decimals = 9 # Constant for perpetual markets base asset amount
136+
137+
for user_data in vat.users.values():
138+
user_account = user_data.get_user_account()
139+
140+
if user_account is None:
141+
continue
142+
143+
user_public_key = str(user_data.user_public_key)
144+
authority = str(user_account.authority)
145+
146+
for position in user_account.perp_positions:
147+
if position.base_asset_amount == 0:
148+
continue
149+
150+
market_index = position.market_index
151+
oracle_price_data = vat.perp_oracles.get(market_index)
152+
153+
if oracle_price_data is None:
154+
print(f"Warning: Missing oracle price data for market_index {market_index} for user {user_public_key}. Skipping position detail.")
155+
continue
156+
157+
try:
158+
base_asset_amount_val = position.base_asset_amount
159+
if base_asset_amount_val is None:
160+
print(f"Warning: Position base_asset_amount is None for user {user_public_key}, market {market_index}. Skipping position detail.")
161+
continue
162+
163+
oracle_price = float(oracle_price_data.price) / PRICE_PRECISION
164+
position_value_usd = (abs(base_asset_amount_val) / (10**decimals)) * oracle_price
165+
base_asset_amount_ui = base_asset_amount_val / (10**decimals)
166+
167+
# Get market symbol
168+
market_symbol = 'N/A'
169+
try:
170+
market_symbol = mainnet_perp_market_configs[market_index].symbol
171+
except (IndexError, AttributeError, KeyError) as symbol_e:
172+
print(f"Warning: Could not find symbol for market_index {market_index}. Error: {symbol_e}")
173+
174+
detailed_positions.append({
175+
'market_index': market_index,
176+
'market_symbol': market_symbol,
177+
'base_asset_amount_ui': base_asset_amount_ui,
178+
'position_value_usd': position_value_usd,
179+
'user_public_key': user_public_key,
180+
'authority': authority
181+
})
182+
183+
except (TypeError, ValueError) as e:
184+
base_val_repr = repr(getattr(position, 'base_asset_amount', 'N/A'))
185+
oracle_price_repr = repr(getattr(oracle_price_data, 'price', 'N/A'))
186+
print(f"Error calculating detailed position data for user {user_public_key}, market {market_index}: {e}. Base: {base_val_repr}, OraclePriceRaw: {oracle_price_repr}. Skipping position detail.")
187+
continue
188+
189+
# Sort by notional value descending
190+
sorted_positions = sorted(detailed_positions, key=lambda x: x['position_value_usd'], reverse=True)
191+
192+
return {
193+
"slot": slot,
194+
"data": sorted_positions,
195+
}
196+
68197
@router.get("/per-authority")
69198
async def get_open_interest_per_authority(request: BackendRequest):
70199
return await _get_open_interest_per_authority(request)
200+
201+
@router.get("/per-account")
202+
async def get_open_interest_per_account(request: BackendRequest):
203+
return await _get_open_interest_per_account(request)
204+
205+
@router.get("/detailed-positions")
206+
async def get_open_positions_detailed(request: BackendRequest):
207+
return await _get_open_positions_detailed(request)

src/page/open_interest_page.py

Lines changed: 142 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,83 @@
11
import streamlit as st
22
import pandas as pd
3+
import time # Added import
34
from lib.api import fetch_api_data
45
from utils import get_current_slot
56

7+
RETRY_DELAY_SECONDS = 5 # Define delay for auto-refresh
8+
9+
def is_processing(result):
10+
"""Checks if the API result indicates backend processing."""
11+
# Check if result is a dictionary and has the specific processing structure
12+
return isinstance(result, dict) and result.get("result") == "processing"
13+
14+
def has_error(result):
15+
"""Checks if the API result indicates an error."""
16+
# Check if result is None or a dictionary with an error message
17+
return result is None or (isinstance(result, dict) and result.get("result") == "error")
18+
619
def open_interest_page():
7-
st.title("Open Interest per Authority")
20+
st.title("Open Interest on Drift")
821

922
try:
10-
# Fetch data from the API endpoint directly
11-
result = fetch_api_data(
23+
# --- 1. Fetch all data ---
24+
result_authority = fetch_api_data(
1225
section="open-interest",
1326
path="per-authority",
14-
retry=True # Enable retry for cache misses/processing states
27+
retry=False # Let the page handle retries via rerun
28+
)
29+
result_account = fetch_api_data(
30+
section="open-interest",
31+
path="per-account",
32+
retry=False
33+
)
34+
result_detailed = fetch_api_data(
35+
section="open-interest",
36+
path="detailed-positions",
37+
retry=False
1538
)
39+
40+
# --- 2. Check for processing state ---
41+
if is_processing(result_authority) or is_processing(result_account) or is_processing(result_detailed):
42+
st.info(f"Backend is processing open interest data. Auto-refreshing in {RETRY_DELAY_SECONDS} seconds...")
43+
# Optionally add more details about which endpoint is processing if needed
44+
# e.g., if is_processing(result_authority): st.caption("Authority data processing...")
45+
with st.spinner("Please wait..."):
46+
time.sleep(RETRY_DELAY_SECONDS)
47+
st.rerun() # Rerun the script to fetch again
48+
return # Stop further execution in this run
49+
50+
# --- 3. Check for errors ---
51+
# Check critical data source first (e.g., authority for total OI metric)
52+
if has_error(result_authority):
53+
error_msg = result_authority['message'] if isinstance(result_authority, dict) else "Could not connect or fetch data."
54+
st.error(f"Failed to fetch essential data (OI by Authority): {error_msg}")
55+
if st.button("Retry Fetch"):
56+
# Optionally clear streamlit cache if fetch_api_data uses it
57+
# st.cache_data.clear()
58+
st.rerun()
59+
return # Stop execution
60+
61+
# Handle potential errors in other non-critical fetches (optional: display warnings instead of stopping)
62+
if has_error(result_account):
63+
st.warning("Could not fetch OI by Account data. This table will be empty.")
64+
# Proceed without account data
1665

17-
if result is None:
18-
st.error("Failed to fetch data from the API.")
19-
return
66+
if has_error(result_detailed):
67+
st.warning("Could not fetch Detailed Open Positions data. This table will be empty.")
68+
# Proceed without detailed data
2069

21-
data = result.get("data", [])
22-
slot = result.get("slot", "N/A")
23-
current_slot = get_current_slot()
70+
# --- 4. Extract data and slot (if all checks passed) ---
71+
# We know result_authority is valid here
72+
data_authority = result_authority.get("data", [])
73+
slot = result_authority.get("slot", "N/A")
74+
75+
# Extract data for others, defaulting to empty list if fetch failed (handled by warnings above)
76+
data_account = result_account.get("data", []) if isinstance(result_account, dict) else []
77+
data_detailed = result_detailed.get("data", []) if isinstance(result_detailed, dict) else []
2478

79+
# --- 5. Display Slot Info ---
80+
current_slot = get_current_slot()
2581
if slot != "N/A" and current_slot:
2682
try:
2783
slot_age = int(current_slot) - int(slot)
@@ -31,31 +87,84 @@ def open_interest_page():
3187
else:
3288
st.info(f"Slot information unavailable. Current slot: {current_slot}")
3389

34-
if not data:
35-
st.warning("No open interest data found.")
36-
return
37-
38-
df = pd.DataFrame(data)
39-
40-
if df.empty:
41-
st.warning("No open interest data to display.")
42-
return
43-
44-
# Rename columns for better readability
45-
df.rename(columns={
46-
'authority': 'User Authority',
47-
'total_open_interest_usd': 'Total Open Interest (USD)'
48-
}, inplace=True)
49-
# Reorder columns
50-
df = df[["User Authority", "Total Open Interest (USD)"]]
51-
# Format USD column with dollar sign and commas
52-
df["Total Open Interest (USD)"] = df["Total Open Interest (USD)"].apply(lambda x: f"${x:,.2f}")
90+
# --- 6. Process and Prepare DataFrames ---
91+
df_authority = pd.DataFrame()
92+
if data_authority:
93+
df_authority = pd.DataFrame(data_authority)
94+
if not df_authority.empty:
95+
df_authority.rename(columns={
96+
'authority': 'User Authority',
97+
'total_open_interest_usd': 'Total Open Interest (USD)'
98+
}, inplace=True)
99+
df_authority = df_authority[["User Authority", "Total Open Interest (USD)"]]
100+
df_authority["Total Open Interest (USD)"] = df_authority["Total Open Interest (USD)"].apply(lambda x: f"${x:,.2f}")
53101

54-
st.metric("Total Authorities with Open Interest", len(df))
55-
st.metric("Total Open Interest (USD)", f"{df['Total Open Interest (USD)'].str.replace('$','').str.replace(',','').astype(float).sum():,.2f}")
102+
df_account = pd.DataFrame()
103+
if data_account:
104+
df_account = pd.DataFrame(data_account)
105+
if not df_account.empty:
106+
df_account.rename(columns={
107+
'user_public_key': 'User Account',
108+
'authority': 'Authority',
109+
'total_open_interest_usd': 'Total Open Interest (USD)'
110+
}, inplace=True)
111+
df_account = df_account[['User Account', 'Authority', 'Total Open Interest (USD)']]
112+
df_account["Total Open Interest (USD)"] = df_account["Total Open Interest (USD)"].apply(lambda x: f"${x:,.2f}")
113+
114+
df_detailed = pd.DataFrame()
115+
if data_detailed:
116+
df_detailed = pd.DataFrame(data_detailed)
117+
if not df_detailed.empty:
118+
df_detailed.rename(columns={
119+
'market_index': 'Market Index',
120+
'market_symbol': 'Market Symbol',
121+
'base_asset_amount_ui': 'Base Asset Amount',
122+
'position_value_usd': 'Notional Value (USD)',
123+
'user_public_key': 'User Account',
124+
'authority': 'Authority'
125+
}, inplace=True)
126+
df_detailed = df_detailed[[
127+
'Market Index', 'Market Symbol', 'Base Asset Amount', 'Notional Value (USD)',
128+
'User Account', 'Authority'
129+
]]
130+
df_detailed['Base Asset Amount'] = df_detailed['Base Asset Amount'].apply(lambda x: f"{x:,.4f}")
131+
df_detailed['Notional Value (USD)'] = df_detailed['Notional Value (USD)'].apply(lambda x: f"${x:,.2f}")
132+
133+
# --- 7. Display Layout (Metrics and DataFrames) ---
134+
total_oi_usd = 0.0
135+
if not df_authority.empty:
136+
try:
137+
# Calculate metric only if df_authority is valid and processed
138+
total_oi_usd = df_authority['Total Open Interest (USD)'].str.replace('[$,]','', regex=True).astype(float).sum()
139+
except Exception as calc_e:
140+
st.warning(f"Could not calculate Total OI metric: {calc_e}")
56141

57-
st.subheader("Open Interest Details")
58-
st.dataframe(df, hide_index=True)
142+
col1, col2 = st.columns(2)
143+
with col1:
144+
st.metric("Total Authorities with Open Interest", len(df_authority) if not df_authority.empty else 0)
145+
with col2:
146+
st.metric("Total Open Interest (USD)", f"${total_oi_usd:,.2f}")
147+
148+
col3, col4 = st.columns(2)
149+
with col3:
150+
st.subheader("OI by Authority")
151+
if not df_authority.empty:
152+
st.dataframe(df_authority, hide_index=True)
153+
else:
154+
st.info("Authority data not available or empty.")
155+
156+
with col4:
157+
st.subheader("OI by Account")
158+
if not df_account.empty:
159+
st.dataframe(df_account, hide_index=True)
160+
else:
161+
st.info("Account data not available or empty.")
162+
163+
st.subheader("Detailed Open Positions")
164+
if not df_detailed.empty:
165+
st.dataframe(df_detailed, hide_index=True, use_container_width=True)
166+
else:
167+
st.info("Detailed position data not available or empty.")
59168

60169
except Exception as e:
61170
st.error(f"An error occurred while displaying the page: {e}")

0 commit comments

Comments
 (0)