Skip to content

Commit 595d0e3

Browse files
authored
Merge pull request #50 from drift-labs:goldhaxx/DPE-3459/fix-risk-dashboard-loading-issues
Goldhaxx/DPE-3459/fix-risk-dashboard-loading-issues
2 parents 6c9dd90 + 25a9a78 commit 595d0e3

File tree

10 files changed

+164
-69
lines changed

10 files changed

+164
-69
lines changed

backend/api/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
market_recommender_api,
99
metadata,
1010
open_interest_api,
11-
pnl,
11+
pnl_api,
1212
positions,
1313
price_shock,
1414
snapshot,
1515
ucache,
1616
user_retention_explorer_api,
1717
user_retention_summary_api,
18-
vaults,
18+
vaults_api,
1919
)

backend/api/pnl.py renamed to backend/api/pnl_api.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
@router.get("/top_pnl")
1010
def get_top_pnl(request: BackendRequest, limit: int = 1000):
1111
vat: Vat = request.state.backend_state.vat
12+
last_oracle_slot = getattr(request.state.backend_state, "last_oracle_slot", 0)
1213

1314
pnl_data = []
1415
for user in vat.users.values():
@@ -32,4 +33,4 @@ def get_top_pnl(request: BackendRequest, limit: int = 1000):
3233
continue
3334

3435
pnl_data.sort(key=lambda x: x["total_pnl"], reverse=True)
35-
return pnl_data[:limit]
36+
return {"pnl": pnl_data[:limit], "slot": last_oracle_slot}

backend/api/vaults.py renamed to backend/api/vaults_api.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ async def get_vault_data(request: BackendRequest):
2020

2121
client = await create_vault_client()
2222
analytics = await client.calculate_analytics()
23+
last_oracle_slot = getattr(request.state.backend_state, "last_oracle_slot", 0)
2324

2425
vaults = []
2526
print(analytics["vaults"][0])
@@ -60,5 +61,6 @@ async def get_vault_data(request: BackendRequest):
6061
vault_depositors[vault_info["pubkey"]] = serializable_depositors
6162

6263
return {
63-
"data": {"analytics": serializable_analytics, "depositors": vault_depositors}
64+
"data": {"analytics": serializable_analytics, "depositors": vault_depositors},
65+
"slot": last_oracle_slot,
6466
}

backend/app.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@
1717
market_recommender_api,
1818
metadata,
1919
open_interest_api,
20-
pnl,
20+
pnl_api,
2121
positions,
2222
price_shock,
2323
snapshot,
2424
ucache,
25-
vaults,
25+
vaults_api,
2626
high_leverage_api,
2727
user_retention_summary_api,
2828
user_retention_explorer_api,
@@ -88,8 +88,8 @@ async def lifespan(app: FastAPI):
8888
app.include_router(snapshot.router, prefix="/api/snapshot", tags=["snapshot"])
8989
app.include_router(ucache.router, prefix="/api/ucache", tags=["ucache"])
9090
app.include_router(deposits_api.router, prefix="/api/deposits", tags=["deposits"])
91-
app.include_router(pnl.router, prefix="/api/pnl", tags=["pnl"])
92-
app.include_router(vaults.router, prefix="/api/vaults", tags=["vaults"])
91+
app.include_router(pnl_api.router, prefix="/api/pnl", tags=["pnl"])
92+
app.include_router(vaults_api.router, prefix="/api/vaults", tags=["vaults"])
9393
app.include_router(positions.router, prefix="/api/positions", tags=["positions"])
9494
app.include_router(market_recommender_api.router, prefix="/api/market-recommender", tags=["market-recommender"])
9595
app.include_router(open_interest_api.router, prefix="/api/open-interest", tags=["open-interest"])

src/main.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@
1313
from page.deposits_page import deposits_page
1414
from page.health import health_page
1515
from page.liquidation_curves_page import liquidation_curves_page
16-
from page.market_inspector import market_inspector_page
16+
from page.market_inspector_page import market_inspector_page
1717
from page.orderbook import orderbook_page
18-
from page.pnl import pnl_page
18+
from page.pnl_page import pnl_page
1919
from page.price_shock import price_shock_cached_page
2020
from page.swap import show as swap_page
21-
from page.vaults import vaults_page
21+
from page.vaults_page import vaults_page
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

src/page/market_inspector.py renamed to src/page/market_inspector_page.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import inspect
77
import pandas as pd
88

9-
from anchorpy import Wallet
9+
from anchorpy.provider import Wallet
1010
from dotenv import load_dotenv
1111
from driftpy.drift_client import DriftClient
1212
from driftpy.market_map.market_map import MarketMap
@@ -30,7 +30,7 @@ def format_pubkey(pubkey: str) -> str:
3030
"""Truncate pubkey for display."""
3131
return f"{str(pubkey)[:10]}...{str(pubkey)[-10:]}"
3232

33-
def format_number(number: int, decimals=6) -> str:
33+
def format_number(number: int | float, decimals=6) -> str:
3434
"""Format large numbers for better readability."""
3535
return f"{number / (10 ** decimals):,.6f}"
3636

@@ -146,7 +146,7 @@ def display_attribute(market_data, attr_path: str, debug_mode: bool = False):
146146

147147
# If it's a Pubkey or similar
148148
if hasattr(val, '__class__') and val.__class__.__name__ == 'Pubkey':
149-
return f"{attr_path}: {format_pubkey(val)}"
149+
return f"{attr_path}: {format_pubkey(str(val))}"
150150

151151
# If it's an int that might represent a 'price' or 'reserve'
152152
if isinstance(val, int):
@@ -201,7 +201,7 @@ def format_complex_object(attr_name, obj):
201201
if isinstance(attr_val, (int, float)) and any(x in attr_name.lower() for x in ["price", "amount", "balance"]):
202202
formatted_value = format_number(attr_val, 6)
203203
elif hasattr(attr_val, '__class__') and attr_val.__class__.__name__ == 'Pubkey':
204-
formatted_value = format_pubkey(attr_val)
204+
formatted_value = format_pubkey(str(attr_val))
205205
else:
206206
formatted_value = str(attr_val)
207207
result.append(f" • {attr_name}: {formatted_value}")
@@ -228,7 +228,7 @@ def format_complex_object(attr_name, obj):
228228
if isinstance(value, (int, float)) and any(x in name.lower() for x in ["price", "amount", "balance"]):
229229
formatted_value = format_number(value, 6)
230230
elif hasattr(value, '__class__') and value.__class__.__name__ == 'Pubkey':
231-
formatted_value = format_pubkey(value)
231+
formatted_value = format_pubkey(str(value))
232232
elif (hasattr(value, '__class__')
233233
and not isinstance(value, (str, int, float, bool, list, dict))
234234
and hasattr(value, '__dict__')):
@@ -278,7 +278,7 @@ async def _fetch_market_maps():
278278
perp_market_map = MarketMap(
279279
MarketMapConfig(
280280
drift_client.program,
281-
MarketType.Perp(),
281+
MarketType.Perp(), # type: ignore
282282
WebsocketConfig(resub_timeout_ms=10000),
283283
connection,
284284
)
@@ -289,7 +289,7 @@ async def _fetch_market_maps():
289289
spot_market_map = MarketMap(
290290
MarketMapConfig(
291291
drift_client.program,
292-
MarketType.Spot(),
292+
MarketType.Spot(), # type: ignore
293293
WebsocketConfig(resub_timeout_ms=10000),
294294
connection,
295295
)

src/page/pnl.py

Lines changed: 0 additions & 40 deletions
This file was deleted.

src/page/pnl_page.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import pandas as pd
2+
import streamlit as st
3+
import time
4+
5+
from lib.api import fetch_api_data
6+
from src.utils import get_current_slot
7+
8+
RETRY_DELAY_SECONDS = 5
9+
10+
11+
def is_processing(result):
12+
"""Checks if the API result indicates backend processing."""
13+
return isinstance(result, dict) and result.get("result") == "processing"
14+
15+
16+
def has_error(result):
17+
"""Checks if the API result indicates an error."""
18+
return result is None or (
19+
isinstance(result, dict) and result.get("result") == "error"
20+
) or ("pnl" not in result if isinstance(result, dict) else False)
21+
22+
23+
def pnl_page():
24+
st.title("Top PnL by User (All Time)")
25+
response = fetch_api_data("pnl", "top_pnl", retry=False)
26+
27+
if is_processing(response):
28+
st.info(
29+
f"Backend is processing PnL data. Auto-refreshing in {RETRY_DELAY_SECONDS} seconds..."
30+
)
31+
with st.spinner("Please wait..."):
32+
time.sleep(RETRY_DELAY_SECONDS)
33+
st.rerun()
34+
return
35+
36+
if has_error(response):
37+
error_msg = (
38+
response["message"]
39+
if isinstance(response, dict) and "message" in response
40+
else "Could not connect or fetch PnL data."
41+
)
42+
st.error(f"Failed to fetch PnL data: {error_msg}")
43+
if st.button("Retry"):
44+
st.rerun()
45+
return
46+
47+
pnl_data = response["pnl"]
48+
if not pnl_data:
49+
st.info("No PnL data available.")
50+
return
51+
52+
slot = response.get("slot", 0)
53+
current_slot = get_current_slot()
54+
if slot > 0 and current_slot > 0:
55+
slot_age = current_slot - slot
56+
st.info(f"Data from slot {slot}, which is {slot_age} slots old.")
57+
58+
df = pd.DataFrame(pnl_data)
59+
for col in ["realized_pnl", "unrealized_pnl", "total_pnl"]:
60+
df[col] = df[col].map("${:,.2f}".format)
61+
62+
csv = df.to_csv(index=False)
63+
st.download_button(
64+
"Download PnL Data CSV", csv, "top_pnl.csv", "text/csv", key="download-pnl"
65+
)
66+
st.dataframe(
67+
df,
68+
height=650,
69+
column_config={
70+
"authority": st.column_config.TextColumn(
71+
"Authority",
72+
help="Authority address",
73+
),
74+
"user_key": st.column_config.TextColumn(
75+
"User Account",
76+
help="User account address",
77+
),
78+
"realized_pnl": st.column_config.NumberColumn("All Time Realized PnL"),
79+
"unrealized_pnl": st.column_config.NumberColumn("Unrealized PnL"),
80+
"total_pnl": st.column_config.NumberColumn("Total PnL"),
81+
},
82+
hide_index=True,
83+
)

src/page/vaults.py renamed to src/page/vaults_page.py

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
11
import pandas as pd
22
import streamlit as st
3+
import time
34

45
from lib.api import fetch_api_data
6+
from src.utils import get_current_slot
7+
8+
RETRY_DELAY_SECONDS = 10
9+
10+
11+
def is_processing(result):
12+
"""Checks if the API result indicates backend processing."""
13+
return isinstance(result, dict) and result.get("result") == "processing"
14+
15+
16+
def has_error(result):
17+
"""Checks if the API result indicates an error."""
18+
return result is None or (
19+
isinstance(result, dict) and result.get("result") == "error"
20+
) or ("data" not in result if isinstance(result, dict) else False)
521

622

723
def vaults_page():
@@ -11,15 +27,37 @@ def vaults_page():
1127
"This page may be out of date up to 30 minutes."
1228
)
1329

14-
with st.spinner("Loading vault data..."):
15-
response = fetch_api_data("vaults", "data", retry=True, max_wait_time=60)
16-
if not response or "data" not in response:
17-
st.error("Failed to load vault data")
18-
return
30+
response = fetch_api_data("vaults", "data", retry=False)
1931

20-
data = response["data"]
21-
analytics = data["analytics"]
22-
all_depositors = data["depositors"]
32+
if is_processing(response):
33+
st.info(
34+
f"Backend is processing vault data. Auto-refreshing in {RETRY_DELAY_SECONDS} seconds..."
35+
)
36+
with st.spinner("Please wait..."):
37+
time.sleep(RETRY_DELAY_SECONDS)
38+
st.rerun()
39+
return
40+
41+
if has_error(response):
42+
error_msg = "Invalid response from backend."
43+
if isinstance(response, dict) and "message" in response:
44+
error_msg = response["message"]
45+
elif has_error(response):
46+
error_msg = "Could not connect or fetch vault data."
47+
st.error(f"Failed to load vault data: {error_msg}")
48+
if st.button("Retry"):
49+
st.rerun()
50+
return
51+
52+
slot = response.get("slot", 0)
53+
current_slot = get_current_slot()
54+
if slot > 0 and current_slot > 0:
55+
slot_age = current_slot - slot
56+
st.info(f"Data from slot {slot}, which is {slot_age} slots old.")
57+
58+
data = response["data"]
59+
analytics = data["analytics"]
60+
all_depositors = data["depositors"]
2361

2462
unique_depositors = set()
2563
for depositors in all_depositors.values():
@@ -60,10 +98,21 @@ def vaults_page():
6098

6199
st.subheader("Vault Depositor Details")
62100

101+
all_vaults_df_indexed = all_vaults_df.set_index("pubkey")
102+
103+
def format_vault_option(pubkey: str) -> str:
104+
try:
105+
vault_info = all_vaults_df_indexed.loc[pubkey]
106+
name = vault_info["name"]
107+
net_deposits = vault_info["true_net_deposits"]
108+
return f"{name} (${net_deposits:,.2f} USD) - {pubkey[:4]}...{pubkey[-4:]}"
109+
except KeyError:
110+
return f"Unknown Vault - {pubkey[:4]}...{pubkey[-4:]}"
111+
63112
selected_vault = st.selectbox(
64113
"Select Vault",
65-
[vault["pubkey"] for vault in all_vaults_df.to_dict(orient="records")],
66-
format_func=lambda x: f"{all_vaults_df[all_vaults_df['pubkey'] == x]['name'].values[0]} (${all_vaults_df[all_vaults_df['pubkey'] == x]['true_net_deposits'].values[0]:,.2f} USD) - {x[:4]}...{x[-4:]}",
114+
all_vaults_df["pubkey"].tolist(),
115+
format_func=format_vault_option,
67116
)
68117

69118
if selected_vault:

src/page/welcome.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def welcome_page():
3535
label="💰 **Deposits** - Track total deposits across all the protocol",
3636
)
3737
st.page_link(
38-
StreamlitPage("page/pnl.py", url_path="pnl"),
38+
StreamlitPage("page/pnl_page.py", url_path="pnl"),
3939
label="💸 **PnL** - Track top trader PnLs",
4040
)
4141

0 commit comments

Comments
 (0)