Skip to content

Commit 29a484f

Browse files
authored
Merge pull request #23 from drift-labs:delist-recommender
Added new coingecko integration for market-recommender backend endpoint.
2 parents d4cd6a9 + cac718e commit 29a484f

11 files changed

+2552
-1017
lines changed

.env.example

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
RPC_URL=""
22
BACKEND_URL=http://localhost:8000
3-
DEV=true
3+
DEV=true
4+
DRIFT_DATA_API_BASE_URL = "https://data.api.drift.trade" # Drift Trade data api base URL. Test at https://data.api.drift.trade/playground
5+
DRIFT_DATA_API_HEADERS = {"Key": "Value"} # Make sure to update this with any headers you may need or remove the key/value placeholders

LEGACY-list-delist-recommender.py

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

backend/api/list_recommender.py

Lines changed: 967 additions & 20 deletions
Large diffs are not rendered by default.

backend/api/market_recommender.py

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

backend/app.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
vaults,
2424
delist_recommender,
2525
list_recommender,
26+
market_recommender,
2627
)
2728
from backend.middleware.cache_middleware import CacheMiddleware
2829
from backend.middleware.readiness import ReadinessMiddleware
@@ -90,8 +91,7 @@ async def lifespan(app: FastAPI):
9091
app.include_router(positions.router, prefix="/api/positions", tags=["positions"])
9192
app.include_router(delist_recommender.router, prefix="/api/delist-recommender", tags=["delist-recommender"])
9293
app.include_router(list_recommender.router, prefix="/api/list-recommender", tags=["list-recommender"])
93-
94-
94+
app.include_router(market_recommender.router, prefix="/api/market-recommender", tags=["market-recommender"])
9595
# NOTE: All other routes should be in /api/* within the /api folder. Routes outside of /api are not exposed in k8s
9696
@app.get("/")
9797
async def root():

backend/tests/test_coingecko.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"""
2+
Simple manual testing script for CoinGecko API functions.
3+
Run this directly to see the output in your terminal.
4+
5+
Usage:
6+
python test_coingecko.py --symbol BTC --coin-id bitcoin --num-tokens 10 --days 30
7+
python test_coingecko.py -s ETH -c ethereum -n 5 -d 7
8+
"""
9+
10+
import argparse
11+
import sys
12+
from pathlib import Path
13+
14+
# Add the parent directory to the Python path so we can import the utils
15+
sys.path.append(str(Path(__file__).parent.parent.parent))
16+
17+
from backend.utils.coingecko_api import (
18+
fetch_coingecko_data,
19+
fetch_all_coingecko_market_data,
20+
fetch_all_coingecko_volumes,
21+
fetch_coingecko_historical_volume
22+
)
23+
24+
def parse_arguments():
25+
parser = argparse.ArgumentParser(description='Test CoinGecko API functions for a specific token')
26+
parser.add_argument('-s', '--symbol', type=str, default='BTC',
27+
help='Token symbol (e.g., BTC, ETH)')
28+
parser.add_argument('-c', '--coin-id', type=str, default='bitcoin',
29+
help='CoinGecko coin ID (e.g., bitcoin, ethereum)')
30+
parser.add_argument('-n', '--num-tokens', type=int, default=10,
31+
help='Number of top tokens to fetch (default: 10, max: 250)')
32+
parser.add_argument('-d', '--days', type=int, default=30,
33+
help='Number of days for historical volume data (default: 30)')
34+
return parser.parse_args()
35+
36+
def run_tests(target_symbol: str, target_coin_id: str, num_tokens: int, days: int):
37+
"""Run tests for CoinGecko API functions."""
38+
print("\n=== Starting CoinGecko API Tests ===\n")
39+
40+
print(f"Fetching data for {num_tokens} top tokens by market cap...")
41+
market_data = fetch_all_coingecko_market_data(num_tokens)
42+
43+
if not market_data:
44+
print("Error: No market data received")
45+
return
46+
47+
total_tokens = len(market_data)
48+
print(f"\nReceived data for {total_tokens} tokens")
49+
if total_tokens < num_tokens:
50+
print(f"Note: Requested {num_tokens} tokens but only received {total_tokens}.")
51+
print("This is normal if you requested more tokens than are available on CoinGecko.")
52+
53+
print("\nTop 10 tokens by market cap (or all if less than 10):")
54+
tokens_to_show = sorted(
55+
market_data.items(),
56+
key=lambda x: x[1].get('market_cap_rank', float('inf'))
57+
)[:min(10, total_tokens)]
58+
59+
for current_coin_id, data in tokens_to_show:
60+
symbol = data.get('symbol', '???')
61+
rank = data.get('market_cap_rank', 'N/A')
62+
price = data.get('current_price', 'N/A')
63+
print(f"#{rank}: {symbol} @ ${price}")
64+
65+
# Test specific coin data if provided
66+
if target_coin_id and target_symbol:
67+
print(f"\nDetailed data for {target_symbol}:")
68+
coin_data = market_data.get(target_coin_id)
69+
70+
if not coin_data:
71+
print(f"Error: Could not find data for {target_symbol} ({target_coin_id})")
72+
return
73+
74+
# Display detailed coin information
75+
print(f"Name: {coin_data.get('name')}")
76+
print(f"Current Price: ${coin_data.get('current_price')}")
77+
print(f"Market Cap Rank: #{coin_data.get('market_cap_rank')}")
78+
print(f"Market Cap: ${coin_data.get('market_cap'):,}")
79+
print(f"Circulating Supply: {coin_data.get('circulating_supply'):,.2f} {target_symbol}")
80+
print(f"All-Time High: ${coin_data.get('ath_price')}")
81+
82+
print(f"\nFetching {days}-day historical volume data...")
83+
volume_data = fetch_coingecko_historical_volume(target_coin_id, number_of_days=days)
84+
if volume_data:
85+
print(f"{days}-day Volume: ${volume_data:,.2f}")
86+
else:
87+
print("Error: Could not fetch volume data")
88+
89+
if __name__ == "__main__":
90+
args = parse_arguments()
91+
print(f"Starting CoinGecko API tests for {args.symbol} ({args.coin_id})...")
92+
print(f"Will fetch top {args.num_tokens} tokens by market cap...")
93+
print(f"Historical volume period: {args.days} days")
94+
run_tests(args.symbol, args.coin_id, args.num_tokens, args.days)
95+
print("\nTests completed!")
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
"""
2+
Detailed testing script for CoinGecko API that aggregates data from multiple endpoints.
3+
Provides comprehensive token information including market data and historical volumes.
4+
5+
Usage:
6+
python test_coingecko_detailed.py --num-tokens 10
7+
python test_coingecko_detailed.py -n 5
8+
"""
9+
10+
import argparse
11+
import json
12+
import sys
13+
from pathlib import Path
14+
from typing import List, Dict
15+
import logging
16+
17+
# Configure logging
18+
logging.basicConfig(
19+
level=logging.INFO,
20+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
21+
)
22+
logger = logging.getLogger(__name__)
23+
24+
# Add the parent directory to the Python path so we can import the utils
25+
sys.path.append(str(Path(__file__).parent.parent.parent))
26+
27+
from backend.utils.coingecko_api import (
28+
fetch_all_coingecko_market_data,
29+
fetch_coingecko_historical_volume,
30+
fetch_all_coingecko_volumes
31+
)
32+
33+
def parse_arguments():
34+
parser = argparse.ArgumentParser(description='Test CoinGecko API with detailed output')
35+
parser.add_argument('-n', '--num-tokens', type=int, default=10,
36+
help='Number of top tokens to fetch (default: 10, max: 250)')
37+
return parser.parse_args()
38+
39+
def format_token_data(market_data: Dict, volume_30d: float) -> Dict:
40+
"""Format token data according to the specified structure."""
41+
return {
42+
"symbol": market_data.get('symbol', ''),
43+
"coingecko_data": {
44+
"coingecko_name": market_data.get('name'),
45+
"coingecko_id": market_data.get('id'),
46+
"coingecko_image_url": market_data.get('image_url'),
47+
"coingecko_current_price": market_data.get('current_price'),
48+
"coingecko_market_cap_rank": market_data.get('market_cap_rank'),
49+
"coingecko_market_cap": market_data.get('market_cap'),
50+
"coingecko_fully_diluted_valuation": market_data.get('fully_diluted_valuation'),
51+
"coingecko_total_volume_24h": market_data.get('total_volume_24h'),
52+
"coingecko_mc_derived": market_data.get('market_cap'), # Same as market_cap for now
53+
"coingecko_circulating": market_data.get('circulating_supply'),
54+
"coingecko_total_supply": market_data.get('total_supply'),
55+
"coingecko_max_supply": market_data.get('max_supply'),
56+
"coingecko_ath_price": market_data.get('ath_price'),
57+
"coingecko_ath_change_percentage": market_data.get('ath_change_percentage'),
58+
"coingecko_volume_30d": volume_30d
59+
}
60+
}
61+
62+
def run_detailed_test(num_tokens: int = 10) -> None:
63+
"""
64+
Run a detailed test of CoinGecko API functionality.
65+
66+
Args:
67+
num_tokens: Number of tokens to fetch data for (default: 10)
68+
"""
69+
logger.info(f"Starting detailed test for {num_tokens} tokens")
70+
71+
# Fetch market data for all tokens
72+
market_data = fetch_all_coingecko_market_data()
73+
if not market_data or isinstance(market_data, dict) and "error" in market_data:
74+
logger.error("Failed to fetch market data")
75+
return
76+
77+
# Convert dictionary to list and sort by market cap
78+
tokens_list = []
79+
for coin_id, token_data in market_data.items():
80+
token_data['id'] = coin_id # Add coin_id to the token data
81+
tokens_list.append(token_data)
82+
83+
# Sort tokens by market cap and take top N
84+
sorted_tokens = sorted(
85+
tokens_list,
86+
key=lambda x: float(x.get("market_cap", 0) or 0), # Handle None values
87+
reverse=True
88+
)[:num_tokens]
89+
90+
# Get list of coin IDs for volume fetch
91+
coin_ids = [token.get("id") for token in sorted_tokens]
92+
logger.info(f"Fetching 30d volume data for top {len(coin_ids)} tokens")
93+
94+
# Fetch 30d volume data for all tokens at once
95+
volume_data = fetch_all_coingecko_volumes(coin_ids)
96+
97+
results = []
98+
total_tokens = len(sorted_tokens)
99+
100+
for idx, token in enumerate(sorted_tokens, 1):
101+
coin_id = token.get("id")
102+
symbol = token.get("symbol", "").upper()
103+
104+
# Get the 30d volume from the volume data
105+
total_volume = volume_data.get(coin_id, 0)
106+
107+
results.append({
108+
"symbol": symbol,
109+
"name": token.get("name"),
110+
"market_cap": token.get("market_cap", 0),
111+
"total_volume_30d": total_volume
112+
})
113+
114+
# Log progress
115+
progress = (idx / total_tokens) * 100
116+
logger.info(f"Progress: {progress:.1f}% ({idx}/{total_tokens})")
117+
118+
# Format and display results
119+
if results:
120+
print("\nResults:")
121+
print(f"{'Symbol':<10} {'Name':<20} {'Market Cap':>15} {'30d Volume':>20}")
122+
print("-" * 65)
123+
124+
for r in results:
125+
print(
126+
f"{r['symbol']:<10} "
127+
f"{r['name'][:20]:<20} "
128+
f"${r['market_cap']:>14,.0f} "
129+
f"${r['total_volume_30d']:>19,.0f}"
130+
)
131+
else:
132+
logger.warning("No results to display")
133+
134+
if __name__ == "__main__":
135+
args = parse_arguments()
136+
print(f"Starting detailed CoinGecko API tests for {args.num_tokens} tokens...")
137+
138+
run_detailed_test(args.num_tokens)
139+
140+
print("\nTests completed!")

0 commit comments

Comments
 (0)