Skip to content

Commit 08a8a27

Browse files
committed
Add Open Interest API and Page Integration
- Introduced the Open Interest API by including the `open_interest_api` router in the backend application, making it accessible at the `/api/open-interest` endpoint. - Updated the Streamlit frontend to include a new page for Open Interest, allowing users to navigate to this section via the URL path `open-interest`, complete with a designated title and icon. - These changes enhance the application's functionality by providing users with access to Open Interest data, contributing to a more comprehensive trading analysis tool.
1 parent 1dc52dd commit 08a8a27

File tree

4 files changed

+145
-0
lines changed

4 files changed

+145
-0
lines changed

backend/api/open_interest_api.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from fastapi import APIRouter
2+
from driftpy.constants.numeric_constants import PRICE_PRECISION
3+
4+
from backend.state import BackendRequest
5+
6+
router = APIRouter()
7+
8+
async def _get_open_interest_per_authority(request: BackendRequest) -> dict:
9+
vat = request.state.backend_state.vat
10+
slot = request.state.backend_state.last_oracle_slot
11+
12+
oi_per_authority = {}
13+
14+
for user_data in vat.users.values():
15+
user_account = user_data.get_user_account()
16+
17+
if user_account is None:
18+
# Optionally log this: print(f"Warning: Skipping user_data as get_user_account() returned None.")
19+
continue
20+
21+
authority = str(user_account.authority)
22+
23+
current_oi_for_authority = oi_per_authority.get(authority, {
24+
'total_open_interest_usd': 0.0,
25+
'authority': authority
26+
})
27+
28+
for position in user_account.perp_positions:
29+
if position.base_asset_amount == 0:
30+
continue
31+
32+
market_index = position.market_index
33+
oracle_price_data = vat.perp_oracles.get(market_index)
34+
35+
if oracle_price_data is None:
36+
print(f"Warning: Missing oracle price data for market_index {market_index} for authority {authority}. Skipping position.")
37+
continue
38+
39+
try:
40+
oracle_price = float(oracle_price_data.price) / PRICE_PRECISION
41+
# All perpetual markets use BASE_PRECISION (10^9) for base asset amounts.
42+
decimals = 9
43+
44+
base_asset_amount_val = position.base_asset_amount
45+
if base_asset_amount_val is None: # Should not happen with base_asset_amount == 0 check, but good for safety
46+
print(f"Warning: Position base_asset_amount is None for authority {authority}, market {market_index}. Skipping position.")
47+
continue
48+
49+
position_value_usd = (abs(base_asset_amount_val) / (10**decimals)) * oracle_price
50+
current_oi_for_authority['total_open_interest_usd'] += position_value_usd
51+
except (TypeError, ValueError) as e:
52+
base_val_repr = repr(getattr(position, 'base_asset_amount', 'N/A'))
53+
oracle_price_repr = repr(getattr(oracle_price_data, 'price', 'N/A'))
54+
print(f"Error calculating position_value_usd for authority {authority}, market {market_index}: {e}. Base: {base_val_repr}, OraclePriceRaw: {oracle_price_repr}. Skipping position.")
55+
continue
56+
57+
if current_oi_for_authority['total_open_interest_usd'] > 0:
58+
oi_per_authority[authority] = current_oi_for_authority
59+
60+
# Filtered values are now implicitly handled by only adding to oi_per_authority if OI > 0
61+
result_list = sorted(list(oi_per_authority.values()), key=lambda x: x['total_open_interest_usd'], reverse=True)
62+
63+
return {
64+
"slot": slot,
65+
"data": result_list,
66+
}
67+
68+
@router.get("/per-authority")
69+
async def get_open_interest_per_authority(request: BackendRequest):
70+
return await _get_open_interest_per_authority(request)

backend/app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
liquidation,
1717
market_recommender_api,
1818
metadata,
19+
open_interest_api,
1920
pnl,
2021
positions,
2122
price_shock,
@@ -88,6 +89,7 @@ async def lifespan(app: FastAPI):
8889
app.include_router(vaults.router, prefix="/api/vaults", tags=["vaults"])
8990
app.include_router(positions.router, prefix="/api/positions", tags=["positions"])
9091
app.include_router(market_recommender_api.router, prefix="/api/market-recommender", tags=["market-recommender"])
92+
app.include_router(open_interest_api.router, prefix="/api/open-interest", tags=["open-interest"])
9193
# NOTE: All other routes should be in /api/* within the /api folder. Routes outside of /api are not exposed in k8s
9294
@app.get("/")
9395
async def root():

src/main.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from page.vaults import vaults_page
2222
from page.welcome import welcome_page
2323
from page.market_recommender_page import market_recommender_page
24+
from page.open_interest_page import open_interest_page
2425

2526
load_dotenv()
2627

@@ -135,6 +136,12 @@ def apply_custom_css(css):
135136
title="Vaults",
136137
icon="🏦",
137138
),
139+
st.Page(
140+
open_interest_page,
141+
url_path="open-interest",
142+
title="Open Interest",
143+
icon="💰",
144+
),
138145
]
139146

140147
pg = st.navigation(

src/page/open_interest_page.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import streamlit as st
2+
import pandas as pd
3+
from lib.api import fetch_api_data
4+
from utils import get_current_slot
5+
6+
def open_interest_page():
7+
st.title("Open Interest per Authority")
8+
9+
try:
10+
# Fetch data from the API endpoint directly
11+
result = fetch_api_data(
12+
section="open-interest",
13+
path="per-authority",
14+
retry=True # Enable retry for cache misses/processing states
15+
)
16+
17+
if result is None:
18+
st.error("Failed to fetch data from the API.")
19+
return
20+
21+
data = result.get("data", [])
22+
slot = result.get("slot", "N/A")
23+
current_slot = get_current_slot()
24+
25+
if slot != "N/A" and current_slot:
26+
try:
27+
slot_age = int(current_slot) - int(slot)
28+
st.info(f"Displaying data for slot {slot} (age: {slot_age} slots)")
29+
except ValueError:
30+
st.info(f"Displaying data for slot {slot}. Current slot: {current_slot}")
31+
else:
32+
st.info(f"Slot information unavailable. Current slot: {current_slot}")
33+
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}")
53+
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}")
56+
57+
st.subheader("Open Interest Details")
58+
st.dataframe(df, hide_index=True)
59+
60+
except Exception as e:
61+
st.error(f"An error occurred while displaying the page: {e}")
62+
import traceback
63+
st.text(traceback.format_exc())
64+
65+
if __name__ == "__main__":
66+
open_interest_page()

0 commit comments

Comments
 (0)