Skip to content

Commit 480f137

Browse files
committed
Update dependencies and enhance open interest API functionality
- Bumped `requests` from version 2.32.3 to 2.32.4 and `driftpy` from version 0.8.57 to 0.8.59 in requirements.txt. - Added a new function `load_markets_from_json` to load market data from a JSON file, improving market data handling. - Updated the `_get_open_interest_per_authority`, `_get_open_interest_per_account`, and `_get_open_positions_detailed` functions to accept an optional `market_name` parameter, allowing for filtering by market. - Introduced a new API endpoint `/markets` to return a list of available market names for the dropdown in the frontend. - Enhanced the open interest page to display total long and short open interest metrics, improving user insights into market positions.
1 parent 5b6ae2b commit 480f137

File tree

3 files changed

+127
-29
lines changed

3 files changed

+127
-29
lines changed

backend/api/open_interest_api.py

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,39 @@
1-
from fastapi import APIRouter
1+
from fastapi import APIRouter, Query
22
from driftpy.constants.numeric_constants import PRICE_PRECISION
3-
from driftpy.constants.config import mainnet_perp_market_configs
3+
from driftpy.constants.perp_markets import mainnet_perp_market_configs
4+
from typing import Optional, List
5+
import json
6+
import logging
47

58
from backend.state import BackendRequest
69

710
router = APIRouter()
11+
logger = logging.getLogger(__name__)
812

9-
async def _get_open_interest_per_authority(request: BackendRequest) -> dict:
13+
def load_markets_from_json(file_path: str):
14+
"""Loads market data from a JSON file and formats it for the API."""
15+
try:
16+
with open(file_path, 'r') as f:
17+
markets_data = json.load(f)
18+
19+
# We need a mapping from marketName to marketIndex
20+
formatted_markets = {market["marketName"]: market["marketIndex"] for market in markets_data}
21+
logger.info(f"Successfully loaded and formatted {len(formatted_markets)} markets from {file_path}")
22+
return formatted_markets
23+
except Exception as e:
24+
logger.error(f"Error loading markets from {file_path}: {e}")
25+
return {}
26+
27+
ALL_MARKETS = load_markets_from_json("shared/markets.json")
28+
29+
async def _get_open_interest_per_authority(request: BackendRequest, market_name: Optional[str] = None) -> dict:
1030
vat = request.state.backend_state.vat
1131
slot = request.state.backend_state.last_oracle_slot
1232

33+
selected_market_index = None
34+
if market_name and market_name != "All" and market_name in ALL_MARKETS:
35+
selected_market_index = ALL_MARKETS[market_name]
36+
1337
oi_per_authority = {}
1438

1539
for user_data in vat.users.values():
@@ -23,13 +47,18 @@ async def _get_open_interest_per_authority(request: BackendRequest) -> dict:
2347

2448
current_oi_for_authority = oi_per_authority.get(authority, {
2549
'total_open_interest_usd': 0.0,
50+
'long_oi_usd': 0.0,
51+
'short_oi_usd': 0.0,
2652
'authority': authority
2753
})
2854

2955
for position in user_account.perp_positions:
3056
if position.base_asset_amount == 0:
3157
continue
3258

59+
if selected_market_index is not None and position.market_index != selected_market_index:
60+
continue
61+
3362
market_index = position.market_index
3463
oracle_price_data = vat.perp_oracles.get(market_index)
3564

@@ -49,6 +78,10 @@ async def _get_open_interest_per_authority(request: BackendRequest) -> dict:
4978

5079
position_value_usd = (abs(base_asset_amount_val) / (10**decimals)) * oracle_price
5180
current_oi_for_authority['total_open_interest_usd'] += position_value_usd
81+
if base_asset_amount_val > 0:
82+
current_oi_for_authority['long_oi_usd'] += position_value_usd
83+
elif base_asset_amount_val < 0:
84+
current_oi_for_authority['short_oi_usd'] += position_value_usd
5285
except (TypeError, ValueError) as e:
5386
base_val_repr = repr(getattr(position, 'base_asset_amount', 'N/A'))
5487
oracle_price_repr = repr(getattr(oracle_price_data, 'price', 'N/A'))
@@ -66,10 +99,14 @@ async def _get_open_interest_per_authority(request: BackendRequest) -> dict:
6699
"data": result_list,
67100
}
68101

69-
async def _get_open_interest_per_account(request: BackendRequest) -> dict:
102+
async def _get_open_interest_per_account(request: BackendRequest, market_name: Optional[str] = None) -> dict:
70103
vat = request.state.backend_state.vat
71104
slot = request.state.backend_state.last_oracle_slot
72105

106+
selected_market_index = None
107+
if market_name and market_name != "All" and market_name in ALL_MARKETS:
108+
selected_market_index = ALL_MARKETS[market_name]
109+
73110
oi_per_account = {}
74111

75112
for user_data in vat.users.values(): # user_data is of type UserMapItem (based on health.py it should have user_public_key)
@@ -92,6 +129,9 @@ async def _get_open_interest_per_account(request: BackendRequest) -> dict:
92129
if position.base_asset_amount == 0:
93130
continue
94131

132+
if selected_market_index is not None and position.market_index != selected_market_index:
133+
continue
134+
95135
market_index = position.market_index
96136
oracle_price_data = vat.perp_oracles.get(market_index)
97137

@@ -127,10 +167,14 @@ async def _get_open_interest_per_account(request: BackendRequest) -> dict:
127167
"data": result_list,
128168
}
129169

130-
async def _get_open_positions_detailed(request: BackendRequest) -> dict:
170+
async def _get_open_positions_detailed(request: BackendRequest, market_name: Optional[str] = None) -> dict:
131171
vat = request.state.backend_state.vat
132172
slot = request.state.backend_state.last_oracle_slot
133173

174+
selected_market_index = None
175+
if market_name and market_name != "All" and market_name in ALL_MARKETS:
176+
selected_market_index = ALL_MARKETS[market_name]
177+
134178
detailed_positions = []
135179
decimals = 9 # Constant for perpetual markets base asset amount
136180

@@ -147,6 +191,9 @@ async def _get_open_positions_detailed(request: BackendRequest) -> dict:
147191
if position.base_asset_amount == 0:
148192
continue
149193

194+
if selected_market_index is not None and position.market_index != selected_market_index:
195+
continue
196+
150197
market_index = position.market_index
151198
oracle_price_data = vat.perp_oracles.get(market_index)
152199

@@ -194,14 +241,21 @@ async def _get_open_positions_detailed(request: BackendRequest) -> dict:
194241
"data": sorted_positions,
195242
}
196243

244+
@router.get("/markets", response_model=List[str])
245+
async def get_available_markets():
246+
"""Returns a list of available market names for the dropdown."""
247+
if not ALL_MARKETS:
248+
return ["All"]
249+
return ["All"] + sorted(list(ALL_MARKETS.keys()))
250+
197251
@router.get("/per-authority")
198-
async def get_open_interest_per_authority(request: BackendRequest):
199-
return await _get_open_interest_per_authority(request)
252+
async def get_open_interest_per_authority(request: BackendRequest, market_name: Optional[str] = Query(None, alias="market_name")):
253+
return await _get_open_interest_per_authority(request, market_name)
200254

201255
@router.get("/per-account")
202-
async def get_open_interest_per_account(request: BackendRequest):
203-
return await _get_open_interest_per_account(request)
256+
async def get_open_interest_per_account(request: BackendRequest, market_name: Optional[str] = Query(None, alias="market_name")):
257+
return await _get_open_interest_per_account(request, market_name)
204258

205259
@router.get("/detailed-positions")
206-
async def get_open_positions_detailed(request: BackendRequest):
207-
return await _get_open_positions_detailed(request)
260+
async def get_open_positions_detailed(request: BackendRequest, market_name: Optional[str] = Query(None, alias="market_name")):
261+
return await _get_open_positions_detailed(request, market_name)

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ pydantic==2.10.6
99
solana==0.36.6
1010
streamlit==1.42.1
1111
uvicorn==0.34.0
12-
requests==2.32.3
12+
requests==2.32.4
1313
plotly==6.0.0
1414
anchorpy==0.21.0
15-
driftpy==0.8.57
15+
driftpy==0.8.59
1616
ccxt==4.2.17
1717
rich>=10.14.0
1818
aiofiles==24.1.0

src/page/open_interest_page.py

Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,48 @@ def has_error(result):
1616
# Check if result is None or a dictionary with an error message
1717
return result is None or (isinstance(result, dict) and result.get("result") == "error")
1818

19+
@st.cache_data(ttl=3600) # Cache for 1 hour
20+
def get_market_list():
21+
"""Fetches the list of available markets for the dropdown."""
22+
return fetch_api_data(section="open-interest", path="markets", params={"bypass_cache": "true"})
23+
1924
def open_interest_page():
2025
st.title("Open Interest on Drift")
2126

27+
market_list = get_market_list()
28+
if not isinstance(market_list, list) or not market_list:
29+
st.error("Could not fetch the list of markets. Defaulting to 'All'.")
30+
market_list = ["All"]
31+
32+
selected_market = st.selectbox(
33+
"Select Market",
34+
options=market_list,
35+
index=0, # Default to "All"
36+
key='oi_market'
37+
)
38+
2239
try:
2340
# --- 1. Fetch all data ---
41+
api_params = {}
42+
if selected_market and selected_market != "All":
43+
api_params['market_name'] = selected_market
44+
2445
result_authority = fetch_api_data(
2546
section="open-interest",
2647
path="per-authority",
48+
params=api_params,
2749
retry=False # Let the page handle retries via rerun
2850
)
2951
result_account = fetch_api_data(
3052
section="open-interest",
3153
path="per-account",
54+
params=api_params,
3255
retry=False
3356
)
3457
result_detailed = fetch_api_data(
3558
section="open-interest",
3659
path="detailed-positions",
60+
params=api_params,
3761
retry=False
3862
)
3963

@@ -96,8 +120,8 @@ def open_interest_page():
96120
'authority': 'User Authority',
97121
'total_open_interest_usd': 'Total Open Interest (USD)'
98122
}, 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}")
123+
# Keep original numeric column for calculation, create a new one for display
124+
df_authority["Total Open Interest (USD) Display"] = df_authority["Total Open Interest (USD)"].apply(lambda x: f"${x:,.2f}")
101125

102126
df_account = pd.DataFrame()
103127
if data_account:
@@ -108,8 +132,8 @@ def open_interest_page():
108132
'authority': 'Authority',
109133
'total_open_interest_usd': 'Total Open Interest (USD)'
110134
}, 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}")
135+
# Create a new column for display
136+
df_account["Total Open Interest (USD) Display"] = df_account["Total Open Interest (USD)"].apply(lambda x: f"${x:,.2f}")
113137

114138
df_detailed = pd.DataFrame()
115139
if data_detailed:
@@ -123,46 +147,66 @@ def open_interest_page():
123147
'user_public_key': 'User Account',
124148
'authority': 'Authority'
125149
}, 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}")
150+
# Create new columns for display
151+
df_detailed['Base Asset Amount Display'] = df_detailed['Base Asset Amount'].apply(lambda x: f"{x:,.4f}")
152+
df_detailed['Notional Value (USD) Display'] = df_detailed['Notional Value (USD)'].apply(lambda x: f"${x:,.2f}")
132153

133154
# --- 7. Display Layout (Metrics and DataFrames) ---
134155
total_oi_usd = 0.0
156+
total_long_oi_usd = 0.0
157+
total_short_oi_usd = 0.0
135158
if not df_authority.empty:
136159
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()
160+
# Calculate metric from the original numeric column
161+
total_oi_usd = df_authority['Total Open Interest (USD)'].sum()
162+
total_long_oi_usd = df_authority['long_oi_usd'].sum()
163+
total_short_oi_usd = df_authority['short_oi_usd'].sum()
139164
except Exception as calc_e:
140165
st.warning(f"Could not calculate Total OI metric: {calc_e}")
141166

142-
col1, col2 = st.columns(2)
167+
col1, col2, col3, col4 = st.columns(4)
143168
with col1:
144169
st.metric("Total Authorities with Open Interest", len(df_authority) if not df_authority.empty else 0)
145170
with col2:
146171
st.metric("Total Open Interest (USD)", f"${total_oi_usd:,.2f}")
172+
with col3:
173+
st.metric("Total Long OI (USD)", f"${total_long_oi_usd:,.2f}")
174+
with col4:
175+
st.metric("Total Short OI (USD)", f"${total_short_oi_usd:,.2f}")
147176

148177
col3, col4 = st.columns(2)
149178
with col3:
150179
st.subheader("OI by Authority")
151180
if not df_authority.empty:
152-
st.dataframe(df_authority, hide_index=True)
181+
# Display only the relevant columns
182+
df_authority_display = df_authority[["User Authority", "Total Open Interest (USD) Display"]]
183+
df_authority_display.columns = ["User Authority", "Total Open Interest (USD)"]
184+
st.dataframe(df_authority_display, hide_index=True)
153185
else:
154186
st.info("Authority data not available or empty.")
155187

156188
with col4:
157189
st.subheader("OI by Account")
158190
if not df_account.empty:
159-
st.dataframe(df_account, hide_index=True)
191+
# Display only the relevant columns
192+
df_account_display = df_account[['User Account', 'Authority', 'Total Open Interest (USD) Display']]
193+
df_account_display.columns = ['User Account', 'Authority', 'Total Open Interest (USD)']
194+
st.dataframe(df_account_display, hide_index=True)
160195
else:
161196
st.info("Account data not available or empty.")
162197

163198
st.subheader("Detailed Open Positions")
164199
if not df_detailed.empty:
165-
st.dataframe(df_detailed, hide_index=True, use_container_width=True)
200+
# Display only the relevant columns
201+
df_detailed_display = df_detailed[[
202+
'Market Index', 'Market Symbol', 'Base Asset Amount Display', 'Notional Value (USD) Display',
203+
'User Account', 'Authority'
204+
]]
205+
df_detailed_display.columns = [
206+
'Market Index', 'Market Symbol', 'Base Asset Amount', 'Notional Value (USD)',
207+
'User Account', 'Authority'
208+
]
209+
st.dataframe(df_detailed_display, hide_index=True, use_container_width=True)
166210
else:
167211
st.info("Detailed position data not available or empty.")
168212

0 commit comments

Comments
 (0)