Skip to content

Commit 96fbb1e

Browse files
committed
Enhance delist recommender with additional exchanges and detailed methodology section
- Added 'htx' to the list of reference spot exchanges and 'bingx' and 'xt' to the list of reference futures exchanges, expanding the data sources for market analysis. - Improved the delist recommender page by introducing a comprehensive methodology section that outlines the scoring process, data sources, and recommendation logic, enhancing transparency and user understanding of the delisting recommendations. - Ensured consistent formatting in the code for better readability and maintainability.
1 parent c258f15 commit 96fbb1e

File tree

2 files changed

+102
-18
lines changed

2 files changed

+102
-18
lines changed

backend/api/delist_recommender.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,11 @@
9999

100100
# Reference exchanges for market data
101101
REFERENCE_SPOT_EXCH = {
102-
'coinbase', 'okx', 'gate', 'kucoin', 'mexc', 'kraken'
102+
'coinbase', 'okx', 'gate', 'kucoin', 'mexc', 'kraken', 'htx'
103103
}
104104

105105
REFERENCE_FUT_EXCH = {
106-
'okx', 'gate', 'mexc', 'htx', 'bitmex'
106+
'okx', 'gate', 'mexc', 'htx', 'bitmex', 'bingx', 'xt'
107107
}
108108

109109
# Add a known decimals mapping before the get_drift_data function

src/page/delist_recommender.py

Lines changed: 100 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,8 @@ def delist_recommender_page():
119119
# The 'index' key contains the symbols in the sample response
120120
if 'index' in results_dict and 'Symbol' not in results_dict:
121121
# If 'index' exists and 'Symbol' doesn't, use 'index' for symbols
122-
df = pd.DataFrame(results_dict)
123-
df = df.rename(columns={'index': 'Symbol'})
122+
df = pd.DataFrame(results_dict)
123+
df = df.rename(columns={'index': 'Symbol'})
124124
elif 'Symbol' in results_dict:
125125
# If 'Symbol' key already exists
126126
df = pd.DataFrame(results_dict)
@@ -137,7 +137,7 @@ def delist_recommender_page():
137137

138138
# Ensure correct sorting by Market Index if available
139139
if 'Market Index' in df.columns:
140-
df = df.sort_values(by='Market Index')
140+
df = df.sort_values(by='Market Index')
141141

142142
# Select and order columns for display
143143
display_cols = [
@@ -164,9 +164,9 @@ def delist_recommender_page():
164164
if 'MC' in df_display.columns: formatters['MC'] = format_large_number
165165
if 'Score' in df_display.columns: formatters['Score'] = "{:.2f}"
166166
if 'Max Lev. on Drift' in df_display.columns:
167-
formatters['Max Lev. on Drift'] = lambda x: f"{int(x)}x" if pd.notna(x) else "N/A"
167+
formatters['Max Lev. on Drift'] = lambda x: f"{int(x)}x" if pd.notna(x) else "N/A"
168168
if 'Market Index' in df_display.columns:
169-
formatters['Market Index'] = lambda x: f"{int(x)}" if pd.notna(x) else "N/A"
169+
formatters['Market Index'] = lambda x: f"{int(x)}" if pd.notna(x) else "N/A"
170170

171171

172172
# Apply formatting and styling
@@ -181,17 +181,17 @@ def delist_recommender_page():
181181
with st.expander("Show Full Data Table"):
182182
# Apply basic formatting to all relevant columns in the original df
183183
full_formatters = {
184-
'OI on Drift': format_large_number,
185-
'Volume on Drift': format_large_number,
186-
'Spot Volume': format_large_number,
187-
'Spot Vol Geomean': format_large_number,
188-
'Fut Volume': format_large_number,
189-
'Fut Vol Geomean': format_large_number,
190-
'MC': format_large_number,
191-
'Oracle Price': lambda x: f"${x:,.6f}" if pd.notna(x) else "N/A",
192-
'Funding Rate % (1h)': format_percentage,
193-
'Max Lev. on Drift': lambda x: f"{int(x)}x" if pd.notna(x) else "N/A",
194-
'Market Index': lambda x: f"{int(x)}" if pd.notna(x) else "N/A",
184+
'OI on Drift': format_large_number,
185+
'Volume on Drift': format_large_number,
186+
'Spot Volume': format_large_number,
187+
'Spot Vol Geomean': format_large_number,
188+
'Fut Volume': format_large_number,
189+
'Fut Vol Geomean': format_large_number,
190+
'MC': format_large_number,
191+
'Oracle Price': lambda x: f"${x:,.6f}" if pd.notna(x) else "N/A",
192+
'Funding Rate % (1h)': format_percentage,
193+
'Max Lev. on Drift': lambda x: f"{int(x)}x" if pd.notna(x) else "N/A",
194+
'Market Index': lambda x: f"{int(x)}" if pd.notna(x) else "N/A",
195195
}
196196
# Add score formatting for all score-related columns
197197
score_cols = [col for col in df.columns if 'Score' in col or 'score' in col.lower()]
@@ -237,6 +237,90 @@ def delist_recommender_page():
237237
# Handle connection error (message already shown by fetch function)
238238
st.warning("Could not retrieve data from the backend. Please ensure it's running and accessible.")
239239

240+
st.markdown("---")
241+
242+
# Methodology Section
243+
st.markdown(
244+
"""
245+
### 📊 Methodology
246+
247+
This section outlines how the delisting recommendations are generated, providing transparency into the data sources and calculations involved.
248+
249+
**Goal:**
250+
251+
The primary objective of this tool is to identify perpetual markets on Drift that may exhibit characteristics suggesting a need for review, potentially leading to leverage reduction or delisting. This is achieved by scoring markets based on several key liquidity, activity, and market size metrics.
252+
253+
**Data Sources:**
254+
255+
The analysis aggregates data from multiple sources:
256+
257+
1. **Drift Protocol Data:**
258+
* **Open Interest (OI):** Calculated by summing the absolute value of long and short positions held by users in each market, converted to USD using the current oracle price. This reflects the total value locked in open contracts on Drift.
259+
* **Trading Volume:** The total USD value of trades executed in the market over the past 30 days, fetched directly from the Drift API.
260+
* **Market Configuration:** Current maximum leverage allowed for each market.
261+
* **Oracle Price:** The current price feed used by Drift for the market.
262+
2. **Centralized Exchange (CEX) Data:**
263+
* **Spot & Futures Volume:** Fetches 30 days of daily OHLCV data from major CEXs (e.g., Coinbase, OKX, Gate, Kucoin, MEXC, Kraken for spot; OKX, Gate, MEXC, HTX, BitMEX for futures) using the `ccxt` library.
264+
* Calculates the *average daily USD volume* across these exchanges for both spot and futures markets.
265+
* Calculates the *geometric mean* of the top 3 exchange volumes for both spot and futures to reward markets with liquidity distributed across multiple venues.
266+
3. **CoinMarketCap (CMC) Data:**
267+
* **Market Capitalization (MC):** Fetches the current market cap (or Fully Diluted Valuation if market cap is unavailable) from the CoinMarketCap API.
268+
269+
**Scoring (Total 80 Points + Boost):**
270+
271+
Markets are scored out of a potential 80 points based on the following categories. Scoring uses exponential ranges, meaning higher values generally receive diminishing point returns. All input metrics (Volume, OI, Market Cap) are processed in their full dollar amounts.
272+
273+
1. **Market Cap Score (20 Points):**
274+
* Assesses the overall size and significance of the asset.
275+
* Based on CMC Market Cap (MC).
276+
* Range: 0 points for less than $1M MC, up to 20 points for >= $5B MC.
277+
2. **Spot Volume Score (20 Points):**
278+
* Measures the liquidity of the asset's spot markets on major CEXs. Higher spot volume generally indicates better price stability and oracle reliability.
279+
* Metrics Used:
280+
* Sum of Average Daily Spot Volume (0-10 points): Range from < $10k/day to >= $1B/day.
281+
* Geometric Mean of Top 3 Average Daily Spot Volumes (0-10 points): Range from < $10k/day to >= $1B/day.
282+
3. **Futures Volume Score (20 Points):**
283+
* Measures the liquidity of the asset's futures markets on major CEXs. Relevant for hedging and price discovery.
284+
* Metrics Used:
285+
* Sum of Average Daily Futures Volume (0-10 points): Range from < $10k/day to >= $1B/day.
286+
* Geometric Mean of Top 3 Average Daily Futures Volumes (0-10 points): Range from < $10k/day to >= $1B/day.
287+
4. **Drift Activity Score (20 Points):**
288+
* Measures how actively the market is traded specifically on Drift.
289+
* Metrics Used:
290+
* 30-Day Drift Trading Volume (0-10 points): Range from < $1k to >= $500M.
291+
* Drift Open Interest (OI) (0-10 points): Range from < $1k to >= $500M.
292+
293+
**Score Boost (Up to 10 Points):**
294+
295+
* Markets listed in `DRIFT_SCORE_BOOST_SYMBOLS` (currently: `DRIFT-PERP`) receive a +10 point boost to their final score.
296+
297+
**Recommendation Logic:**
298+
299+
The final recommendation ('Keep', 'Decrease Leverage', 'Delist') is determined by comparing the market's **Total Score** against dynamic lower bounds (`SCORE_LB`) that depend on the market's **Current Maximum Leverage** on Drift:
300+
301+
* **Score Lower Bounds (`SCORE_LB`):**
302+
* Leverage <= 5x: Lower Bound = 37 points
303+
* Leverage <= 10x: Lower Bound = 48 points
304+
* Leverage <= 20x: Lower Bound = 60 points
305+
* *(Note: Leverage 0x has a lower bound of 0)*
306+
307+
* **Decision Process:**
308+
1. Determine the applicable lower bound based on the market's current max leverage.
309+
2. If `Total Score < Applicable Lower Bound`:
310+
* If `Current Max Leverage > 5x`: Recommend **Decrease Leverage**.
311+
* If `Current Max Leverage <= 5x`: Recommend **Delist**.
312+
3. If `Total Score >= Applicable Lower Bound`: Recommend **Keep**.
313+
314+
This approach aims to be more conservative with lower-leverage markets, requiring a higher relative score to maintain their status, while allowing higher-leverage markets more leeway before recommending a decrease.
315+
316+
**Important Notes:**
317+
318+
* This tool provides recommendations based on quantitative data. Qualitative factors (e.g., project roadmap, team, regulatory concerns) are not considered.
319+
* Data fetching from external APIs (CEXs, CMC) can occasionally fail or be incomplete, which might affect scores. The system attempts fallbacks where possible but defaults to zero values if data is unavailable.
320+
* Prediction market symbols (e.g., `TRUMP-WIN-2024-BET`) are explicitly excluded from the analysis.
321+
"""
322+
)
323+
240324
st.markdown("---")
241325
st.caption("Data is cached for 10 minutes. Click Refresh to fetch the latest data from the backend.")
242326

0 commit comments

Comments
 (0)