Skip to content

Commit cc70cf1

Browse files
authored
Merge pull request #16 from drift-labs/delist-recommender
delister branch merge back to master
2 parents cc74145 + 57e93e2 commit cc70cf1

File tree

8 files changed

+5127
-3
lines changed

8 files changed

+5127
-3
lines changed

LEGACY-list-delist-recommender.py

Lines changed: 993 additions & 0 deletions
Large diffs are not rendered by default.

backend/api/delist_recommender.py

Lines changed: 2206 additions & 0 deletions
Large diffs are not rendered by default.

backend/api/positions.py

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
from driftpy.constants import BASE_PRECISION, PRICE_PRECISION, QUOTE_SPOT_MARKET_INDEX, SPOT_BALANCE_PRECISION
2+
from driftpy.pickle.vat import Vat
3+
from driftpy.types import is_variant
4+
from fastapi import APIRouter
5+
import logging
6+
from collections import defaultdict
7+
8+
from backend.state import BackendRequest
9+
10+
router = APIRouter()
11+
logger = logging.getLogger(__name__)
12+
13+
14+
def format_number(number: float, decimals: int = 4, use_commas: bool = True) -> str:
15+
"""Format a number with proper decimal places and optional comma separators"""
16+
if abs(number) >= 1e6:
17+
# For large numbers, use millions format
18+
return f"{number/1e6:,.{decimals}f}M"
19+
elif abs(number) >= 1e3 and use_commas:
20+
return f"{number:,.{decimals}f}"
21+
else:
22+
return f"{number:.{decimals}f}"
23+
24+
25+
async def _get_aggregated_positions(vat: Vat) -> dict:
26+
"""
27+
Get aggregated positions for all users in the system.
28+
29+
Args:
30+
vat: The Vat instance containing user and market data
31+
32+
Returns:
33+
dict: Dictionary containing aggregated position data including:
34+
- total_unique_authorities: Number of unique user authorities
35+
- total_sub_accounts: Total number of sub-accounts
36+
- total_net_value: Total net value across all accounts
37+
- perp_markets: Dictionary of perpetual market aggregates
38+
- spot_markets: Dictionary of spot market aggregates
39+
- errors: List of any errors encountered during processing
40+
"""
41+
# Initialize aggregation containers
42+
perp_aggregates = defaultdict(lambda: {
43+
"market_name": "",
44+
"total_long_usd": 0.0,
45+
"total_short_usd": 0.0,
46+
"total_lp_shares": 0,
47+
"current_price": None, # Changed to None as default
48+
"unique_users": set(),
49+
"errors": [] # Track errors per market
50+
})
51+
52+
spot_aggregates = defaultdict(lambda: {
53+
"market_name": "",
54+
"total_deposits_native": 0.0,
55+
"total_borrows_native": 0.0,
56+
"total_deposits_usd": 0.0,
57+
"total_borrows_usd": 0.0,
58+
"token_price": None, # Changed to None as default
59+
"decimals": 0,
60+
"unique_users": set(),
61+
"errors": [] # Track errors per market
62+
})
63+
64+
total_net_value = 0.0
65+
total_sub_accounts = 0
66+
unique_authorities = set()
67+
global_errors = [] # Track global errors
68+
69+
# Log the available oracles for debugging
70+
logger.info(f"Available perp oracles: {list(vat.perp_oracles.keys())}")
71+
logger.info(f"Available spot oracles: {list(vat.spot_oracles.keys())}")
72+
73+
user_count = sum(1 for _ in vat.users.values())
74+
logger.info(f"Processing {user_count} users")
75+
76+
# Process all users
77+
for user in vat.users.values():
78+
try:
79+
user_account = user.get_user_account()
80+
authority = str(user_account.authority)
81+
unique_authorities.add(authority)
82+
total_sub_accounts += 1
83+
84+
try:
85+
total_net_value += user.get_net_usd_value() / 1e6
86+
except Exception as e:
87+
logger.warning(f"Error calculating net value for user {authority}: {str(e)}")
88+
89+
# Process perpetual positions
90+
perp_positions = user.get_active_perp_positions()
91+
for position in perp_positions:
92+
try:
93+
market = user.drift_client.get_perp_market_account(position.market_index)
94+
market_name = bytes(market.name).decode('utf-8').strip('\x00')
95+
96+
agg = perp_aggregates[position.market_index]
97+
agg["market_name"] = market_name
98+
99+
oracle_price_data = vat.perp_oracles.get(position.market_index)
100+
if oracle_price_data is None:
101+
error_msg = f"Oracle not found for perp market {position.market_index}"
102+
if error_msg not in agg["errors"]:
103+
agg["errors"].append(error_msg)
104+
logger.warning(error_msg)
105+
continue
106+
107+
agg["current_price"] = oracle_price_data.price / PRICE_PRECISION
108+
109+
position_value = abs(user.get_perp_position_value(
110+
position.market_index,
111+
oracle_price_data,
112+
include_open_orders=True
113+
) / PRICE_PRECISION)
114+
115+
base_asset_amount = position.base_asset_amount / BASE_PRECISION
116+
if base_asset_amount > 0:
117+
agg["total_long_usd"] += position_value
118+
else:
119+
agg["total_short_usd"] += position_value
120+
121+
agg["total_lp_shares"] += position.lp_shares / BASE_PRECISION
122+
agg["unique_users"].add(authority)
123+
except Exception as e:
124+
error_msg = f"Error processing perp position for market {position.market_index}: {str(e)}"
125+
logger.warning(error_msg)
126+
if error_msg not in perp_aggregates[position.market_index]["errors"]:
127+
perp_aggregates[position.market_index]["errors"].append(error_msg)
128+
129+
# Process spot positions
130+
spot_positions = user.get_active_spot_positions()
131+
for position in spot_positions:
132+
try:
133+
market = user.drift_client.get_spot_market_account(position.market_index)
134+
market_name = bytes(market.name).decode('utf-8').strip('\x00')
135+
136+
agg = spot_aggregates[position.market_index]
137+
agg["market_name"] = market_name
138+
agg["decimals"] = market.decimals
139+
140+
token_amount = user.get_token_amount(position.market_index)
141+
formatted_amount = token_amount / (10 ** market.decimals)
142+
143+
if position.market_index == QUOTE_SPOT_MARKET_INDEX:
144+
token_price = 1.0
145+
token_value = abs(formatted_amount)
146+
else:
147+
oracle_price_data = vat.spot_oracles.get(position.market_index)
148+
if oracle_price_data is None:
149+
error_msg = f"Oracle not found for spot market {position.market_index}"
150+
if error_msg not in agg["errors"]:
151+
agg["errors"].append(error_msg)
152+
logger.warning(error_msg)
153+
continue
154+
155+
token_price = oracle_price_data.price / PRICE_PRECISION
156+
if token_amount < 0:
157+
token_value = abs(user.get_spot_market_liability_value(
158+
market_index=position.market_index,
159+
include_open_orders=True
160+
) / PRICE_PRECISION)
161+
else:
162+
token_value = abs(user.get_spot_market_asset_value(
163+
market_index=position.market_index,
164+
include_open_orders=True
165+
) / PRICE_PRECISION)
166+
167+
agg["token_price"] = token_price
168+
169+
if token_amount > 0:
170+
agg["total_deposits_native"] += formatted_amount
171+
agg["total_deposits_usd"] += token_value
172+
else:
173+
agg["total_borrows_native"] += abs(formatted_amount)
174+
agg["total_borrows_usd"] += token_value
175+
176+
agg["unique_users"].add(authority)
177+
except Exception as e:
178+
error_msg = f"Error processing spot position for market {position.market_index}: {str(e)}"
179+
logger.warning(error_msg)
180+
if error_msg not in spot_aggregates[position.market_index]["errors"]:
181+
spot_aggregates[position.market_index]["errors"].append(error_msg)
182+
183+
except Exception as e:
184+
error_msg = f"Error processing user {authority}: {str(e)}"
185+
logger.warning(error_msg)
186+
global_errors.append(error_msg)
187+
continue
188+
189+
# Convert sets to counts for JSON serialization
190+
for agg in perp_aggregates.values():
191+
agg["unique_users"] = len(agg["unique_users"])
192+
for agg in spot_aggregates.values():
193+
agg["unique_users"] = len(agg["unique_users"])
194+
195+
# Sort markets by index
196+
sorted_perp_markets = dict(sorted(perp_aggregates.items(), key=lambda x: int(x[0])))
197+
sorted_spot_markets = dict(sorted(spot_aggregates.items(), key=lambda x: int(x[0])))
198+
199+
return {
200+
"total_unique_authorities": len(unique_authorities),
201+
"total_sub_accounts": total_sub_accounts,
202+
"total_net_value": total_net_value,
203+
"perp_markets": sorted_perp_markets,
204+
"spot_markets": sorted_spot_markets,
205+
"global_errors": global_errors
206+
}
207+
208+
209+
@router.get("/aggregated")
210+
async def get_aggregated_positions(request: BackendRequest):
211+
"""
212+
Get aggregated positions across all users in the Drift Protocol.
213+
214+
This endpoint calculates and returns aggregated position data including:
215+
- Total unique authorities and sub-accounts
216+
- Total net value across all accounts
217+
- Per-market aggregates for both perpetual and spot markets
218+
- Position values, user counts, and market details
219+
- Error tracking for missing oracles or calculation issues
220+
221+
Returns:
222+
dict: Aggregated position data across all markets and users
223+
"""
224+
return await _get_aggregated_positions(request.state.backend_state.vat)

backend/app.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616
liquidation,
1717
metadata,
1818
pnl,
19+
positions,
1920
price_shock,
2021
snapshot,
2122
ucache,
2223
vaults,
24+
delist_recommender,
2325
)
2426
from backend.middleware.cache_middleware import CacheMiddleware
2527
from backend.middleware.readiness import ReadinessMiddleware
@@ -84,6 +86,8 @@ async def lifespan(app: FastAPI):
8486
app.include_router(deposits.router, prefix="/api/deposits", tags=["deposits"])
8587
app.include_router(pnl.router, prefix="/api/pnl", tags=["pnl"])
8688
app.include_router(vaults.router, prefix="/api/vaults", tags=["vaults"])
89+
app.include_router(positions.router, prefix="/api/positions", tags=["positions"])
90+
app.include_router(delist_recommender.router, prefix="/api/delist-recommender", tags=["delist-recommender"])
8791

8892

8993
# NOTE: All other routes should be in /api/* within the /api folder. Routes outside of /api are not exposed in k8s

requirements.txt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ numpy==1.26.4
66
pandas==2.2.3
77
python-dotenv==1.0.1
88
pydantic==2.10.6
9-
solana==0.35.1
9+
solana==0.36.1
1010
streamlit==1.42.1
1111
uvicorn==0.34.0
1212
requests==2.32.3
1313
plotly==6.0.0
14-
anchorpy==0.20.1
15-
driftpy>=0.8.35
14+
anchorpy==0.21.0
15+
driftpy>=0.8.40
16+
ccxt==4.2.17
17+
rich>=10.14.0

src/main.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
from page.swap import show as swap_page
2121
from page.vaults import vaults_page
2222
from page.welcome import welcome_page
23+
from page.list_delist_recommender import list_delist_recommender_page
24+
from page.delist_recommender import delist_recommender_page
2325

2426
load_dotenv()
2527

@@ -86,6 +88,18 @@ def apply_custom_css(css):
8688
title="Deposits",
8789
icon="💰",
8890
),
91+
st.Page(
92+
delist_recommender_page,
93+
url_path="delist-recommender",
94+
title="Delist Recommender",
95+
icon="🚫",
96+
),
97+
st.Page(
98+
list_delist_recommender_page,
99+
url_path="list-delist-recommender",
100+
title="List Delist Recommender",
101+
icon="🔍",
102+
),
89103
]
90104

91105
risk_pages = [

0 commit comments

Comments
 (0)