Skip to content

Commit 2412676

Browse files
authored
Merge pull request #27 from drift-labs:goldhaxx/market-recommender
Enhance Market Recommender and Introduce Caching Mechanism
2 parents 9b09b62 + 1e7bfd9 commit 2412676

File tree

6 files changed

+620
-91
lines changed

6 files changed

+620
-91
lines changed

backend/api/market_recommender.py

Lines changed: 104 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
# This is the new version of the market recommender which makes use of the coingecko API via the /backend/utils/coingecko_api.py utility script.
22

3+
import os
4+
import time
35
import logging
4-
from typing import Dict, List
6+
from datetime import datetime, timedelta
7+
from typing import Dict, List, Optional, Tuple, Any
58
from fastapi import APIRouter
6-
from backend.utils.coingecko_api import fetch_all_coingecko_market_data, fetch_all_coingecko_volumes
79
from driftpy.pickle.vat import Vat
810
from backend.state import BackendRequest
9-
import os
10-
from datetime import datetime, timedelta
11+
from backend.utils.cache_utils import ttl_cache
12+
from backend.utils.coingecko_api import (
13+
fetch_all_coingecko_market_data,
14+
fetch_all_coingecko_volumes
15+
)
16+
from backend.utils.drift_data_api import (
17+
fetch_drift_data_api_data,
18+
clean_market_name
19+
)
1120

1221
# --- Constants ---
1322
PRICE_PRECISION = 1e6 # Add price precision constant
@@ -16,7 +25,32 @@
1625
}
1726

1827
# Symbols to ignore
19-
IGNORE_SYMBOLS = ['USDT', 'USDC']
28+
IGNORED_SYMBOLS = ['USDT', 'USDC', 'USDE', 'USDS', 'SUSDS']
29+
30+
# Basket market prefixes to strip when normalizing symbols
31+
BASKET_MARKET_PREFIXES = {
32+
'1M': '', # e.g., 1MPEPE -> PEPE
33+
'1K': '', # e.g., 1KWEN -> WEN
34+
}
35+
36+
# Drift perp markets to ignore
37+
IGNORED_DRIFT_PERP_MARKETS = [
38+
{"index": 39, "name": "REPUBLICAN-POPULAR-AND-WIN-BET"},
39+
{"index": 38, "name": "FED-CUT-50-SEPT-2024-BET"},
40+
{"index": 50, "name": "LNDO-WIN-F1-24-US-GP"},
41+
{"index": 37, "name": "KAMALA-POPULAR-VOTE-2024-BET"},
42+
{"index": 49, "name": "VRSTPN-WIN-F1-24-DRVRS-CHMP"},
43+
{"index": 68, "name": "NBAFINALS25-BOS-BET"},
44+
{"index": 67, "name": "NBAFINALS25-OKC-BET"},
45+
{"index": 40, "name": "BREAKPOINT-IGGYERIC-BET"},
46+
{"index": 48, "name": "WLF-5B-1W-BET"},
47+
{"index": 58, "name": "SUPERBOWL-LIX-CHIEFS-BET"},
48+
{"index": 57, "name": "SUPERBOWL-LIX-LIONS-BET"},
49+
{"index": 43, "name": "LANDO-F1-SGP-WIN-BET"},
50+
{"index": 41, "name": "DEMOCRATS-WIN-MICHIGAN-BET"},
51+
{"index": 46, "name": "WARWICK-FIGHT-WIN-BET"},
52+
{"index": 36, "name": "TRUMP-WIN-2024-BET"}
53+
]
2054

2155
# Symbol conversions
2256
SYMBOL_CONVERSIONS = {
@@ -47,30 +81,34 @@ async def get_market_data(request: BackendRequest, number_of_tokens: int = 2):
4781
"""
4882
return main(request.state.backend_state.vat, number_of_tokens)
4983

84+
@ttl_cache(ttl_seconds=43200) # Cache for 12 hours
5085
def fetch_coingecko_market_data(number_of_tokens: int = 2) -> List[Dict]:
5186
"""
5287
Fetches market data for top N coins from CoinGecko API.
88+
Responses are cached for 12 hours.
5389
5490
Args:
5591
number_of_tokens (int, optional): Number of tokens to fetch data for. Defaults to 2.
5692
5793
Returns:
5894
List of dictionaries containing market data for each coin
5995
"""
60-
logger.info(f"Fetching market data for top {number_of_tokens} coins from CoinGecko...")
96+
logger.info(f"🔍 Attempting to fetch market data for top {number_of_tokens} coins...")
6197

6298
try:
63-
# Fetch market data
99+
# Fetch market data (this call is now cached)
100+
logger.info("📊 Fetching CoinGecko market data...")
64101
market_data = fetch_all_coingecko_market_data(number_of_tokens)
65102

66103
if not market_data:
67-
logger.error("Failed to fetch market data from CoinGecko")
104+
logger.error("Failed to fetch market data from CoinGecko")
68105
return []
69106

70107
# Get list of coin IDs for volume fetching
71108
coin_ids = list(market_data.keys())
109+
logger.info(f"📈 Found {len(coin_ids)} coins, fetching volume data...")
72110

73-
# Fetch 30-day volume data
111+
# Fetch 30-day volume data (this call is now cached)
74112
volume_data = fetch_all_coingecko_volumes(coin_ids)
75113

76114
# Format response data
@@ -97,16 +135,16 @@ def fetch_coingecko_market_data(number_of_tokens: int = 2) -> List[Dict]:
97135
}
98136
}
99137
formatted_data.append(formatted_entry)
100-
logger.info(f"Processed market data for {formatted_entry['symbol']}")
138+
logger.debug(f"Processed market data for {formatted_entry['symbol']}")
101139
except Exception as e:
102-
logger.error(f"Error formatting data for coin {coin_id}: {e}")
140+
logger.error(f"Error formatting data for coin {coin_id}: {e}")
103141
continue
104142

105-
logger.info(f"Successfully processed market data for {len(formatted_data)} coins")
143+
logger.info(f"Successfully processed market data for {len(formatted_data)} coins")
106144
return formatted_data
107145

108146
except Exception as e:
109-
logger.error(f"Error in fetch_coingecko_market_data: {e}", exc_info=True)
147+
logger.error(f"Error in fetch_coingecko_market_data: {e}", exc_info=True)
110148
return []
111149

112150
def fetch_driftpy_data(vat: Vat) -> Dict:
@@ -173,17 +211,22 @@ def fetch_driftpy_data(vat: Vat) -> Dict:
173211
try:
174212
# Get market name and clean it
175213
market_name = bytes(market.data.name).decode('utf-8').strip('\x00').strip()
214+
# Check if this market should be ignored
215+
if any(ignored_market["index"] == market.data.market_index for ignored_market in IGNORED_DRIFT_PERP_MARKETS):
216+
logger.info(f"Skipping ignored market: {market_name} (index: {market.data.market_index})")
217+
continue
218+
176219
# Preserve original case for API calls
177220
original_market_name = market_name
178221

179-
# Extract symbol from market name (e.g., "BTC-PERP" -> "BTC")
180-
# Keep original case for API calls, but use uppercase for dictionary keys
222+
# Extract symbol from market name and handle basket markets
223+
# e.g., "1MPEPE-PERP" -> "PEPE", "1KWEN-PERP" -> "WEN"
181224
symbol_parts = market_name.split('-')
182225
if len(symbol_parts) > 0:
183-
# Use uppercase for normalized symbol (dictionary keys)
184-
normalized_symbol = symbol_parts[0].upper().strip()
226+
# Clean the symbol part (removes basket prefixes)
227+
normalized_symbol = clean_market_name(symbol_parts[0]).upper()
185228
else:
186-
normalized_symbol = market_name.upper().strip()
229+
normalized_symbol = clean_market_name(market_name).upper()
187230

188231
# Get oracle price from perp_oracles
189232
market_price = vat.perp_oracles.get(market.data.market_index)
@@ -382,13 +425,13 @@ def fetch_api_page(url: str, retries: int = 5) -> Dict:
382425

383426
return {"success": False, "records": [], "meta": {"totalRecords": 0}}
384427

385-
def fetch_market_trades(symbol: str, start_date: datetime, end_date: datetime = None) -> List[Dict]:
428+
def fetch_market_trades(market_name: str, start_date: datetime, end_date: datetime = None) -> List[Dict]:
386429
"""
387-
Fetch market candle data for a symbol using the candles endpoint.
388-
This replaces the inefficient day-by-day trade fetching with a single API call.
430+
Fetch market candle data for a market using the candles endpoint.
431+
Uses the original market name (e.g., "1MPEPE-PERP") for API calls.
389432
390433
Args:
391-
symbol: Market symbol (e.g., "SOL-PERP")
434+
market_name: Original market name (e.g., "1MPEPE-PERP", "SOL-PERP")
392435
start_date: Start date for candle data
393436
end_date: End date for candle data (defaults to current time)
394437
@@ -398,8 +441,8 @@ def fetch_market_trades(symbol: str, start_date: datetime, end_date: datetime =
398441
if end_date is None:
399442
end_date = datetime.now()
400443

401-
# Log the start of fetching for this symbol
402-
logger.info(f"Starting to fetch candle data for {symbol} from {start_date} to {end_date}")
444+
# Log the start of fetching for this market
445+
logger.info(f"Starting to fetch candle data for {market_name} from {start_date} to {end_date}")
403446

404447
# Convert dates to Unix timestamps (seconds)
405448
# NOTE: For the Drift API, startTs should be the more recent timestamp (end_date)
@@ -416,7 +459,7 @@ def fetch_market_trades(symbol: str, start_date: datetime, end_date: datetime =
416459

417460
# Use the candles endpoint with daily resolution (D)
418461
# The API expects: startTs = most recent, endTs = oldest (reverse of what might be expected)
419-
url = f"{DRIFT_DATA_API_BASE}/market/{symbol}/candles/D?startTs={start_ts}&endTs={end_ts}&limit={min(days, 31)}"
462+
url = f"{DRIFT_DATA_API_BASE}/market/{market_name}/candles/D?startTs={start_ts}&endTs={end_ts}&limit={min(days, 31)}"
420463

421464
logger.info(f"Requesting candles with startTs={start_ts} (now), endTs={end_ts} (past)")
422465

@@ -425,19 +468,19 @@ def fetch_market_trades(symbol: str, start_date: datetime, end_date: datetime =
425468

426469
if data.get("success") and "records" in data:
427470
records_count = len(data["records"])
428-
logger.info(f"Successfully fetched {records_count} candles for {symbol}")
471+
logger.info(f"Successfully fetched {records_count} candles for {market_name}")
429472
return data["records"]
430473
else:
431-
logger.warning(f"Failed to fetch candle data for {symbol}")
474+
logger.warning(f"Failed to fetch candle data for {market_name}")
432475
return []
433476

434-
def calculate_market_volume(symbol: str) -> dict:
477+
def calculate_market_volume(market_name: str) -> dict:
435478
"""
436479
Calculate total trading volume for a market over the past 30 days using candle data.
437-
Returns both quote volume (in USD) and base volume (in token units).
480+
Uses the original market name for API calls.
438481
439482
Args:
440-
symbol: Market symbol (e.g., "SOL-PERP")
483+
market_name: Original market name (e.g., "1MPEPE-PERP", "SOL-PERP")
441484
442485
Returns:
443486
dict: Dictionary with quote_volume and base_volume
@@ -446,7 +489,7 @@ def calculate_market_volume(symbol: str) -> dict:
446489
start_date = end_date - timedelta(days=DAYS_TO_CONSIDER)
447490

448491
try:
449-
candles = fetch_market_trades(symbol, start_date, end_date)
492+
candles = fetch_market_trades(market_name, start_date, end_date)
450493

451494
total_quote_volume = 0.0
452495
total_base_volume = 0.0
@@ -462,7 +505,7 @@ def calculate_market_volume(symbol: str) -> dict:
462505
except (ValueError, TypeError):
463506
continue
464507

465-
logger.info(f"Calculated {DAYS_TO_CONSIDER}-day volumes for {symbol}: "
508+
logger.info(f"Calculated {DAYS_TO_CONSIDER}-day volumes for {market_name}: "
466509
f"${total_quote_volume:,.2f} (quote), {total_base_volume:,.2f} (base)")
467510

468511
return {
@@ -471,7 +514,7 @@ def calculate_market_volume(symbol: str) -> dict:
471514
}
472515

473516
except Exception as e:
474-
logger.error(f"Error calculating volume for {symbol}: {str(e)}")
517+
logger.error(f"Error calculating volume for {market_name}: {str(e)}")
475518
return {
476519
"quote_volume": 0.0,
477520
"base_volume": 0.0
@@ -486,7 +529,10 @@ def calculate_market_volume(symbol: str) -> dict:
486529
logger.info(f"Processing {len(discovered_markets)} discovered markets")
487530

488531
for symbol, market_info in discovered_markets.items():
489-
drift_markets[symbol] = {
532+
# Clean the symbol to handle basket markets
533+
normalized_symbol = clean_market_name(symbol).upper()
534+
535+
drift_markets[normalized_symbol] = {
490536
"drift_is_listed_spot": market_info.get("drift_is_listed_spot", "false"),
491537
"drift_is_listed_perp": market_info.get("drift_is_listed_perp", "false"),
492538
"drift_perp_markets": {},
@@ -502,11 +548,17 @@ def calculate_market_volume(symbol: str) -> dict:
502548
total_quote_volume = 0.0
503549
total_base_volume = 0.0
504550
for perp_market_name in market_info.get("drift_perp_markets", {}):
551+
# Check if this market should be ignored
552+
market_index = next((market["index"] for market in IGNORED_DRIFT_PERP_MARKETS if market["name"] == perp_market_name), None)
553+
if market_index is not None:
554+
logger.info(f"Skipping ignored perp market: {perp_market_name}")
555+
continue
556+
505557
# Use original market name (case-preserved) for API call
506558
logger.info(f"Calculating volume for perp market: {perp_market_name} (preserving case)")
507559
volume_data = calculate_market_volume(perp_market_name) # Returns dict with quote and base volumes
508560

509-
drift_markets[symbol]["drift_perp_markets"][perp_market_name] = {
561+
drift_markets[normalized_symbol]["drift_perp_markets"][perp_market_name] = {
510562
"drift_perp_oracle_price": market_info["drift_perp_markets"][perp_market_name].get("drift_perp_oracle_price", 0.0),
511563
"drift_perp_quote_volume_30d": volume_data["quote_volume"],
512564
"drift_perp_base_volume_30d": volume_data["base_volume"],
@@ -522,7 +574,7 @@ def calculate_market_volume(symbol: str) -> dict:
522574
logger.info(f"Calculating volume for spot market: {spot_market_name} (preserving case)")
523575
volume_data = calculate_market_volume(spot_market_name) # Returns dict with quote and base volumes
524576

525-
drift_markets[symbol]["drift_spot_markets"][spot_market_name] = {
577+
drift_markets[normalized_symbol]["drift_spot_markets"][spot_market_name] = {
526578
"drift_spot_oracle_price": market_info["drift_spot_markets"][spot_market_name].get("drift_spot_oracle_price", 0.0),
527579
"drift_spot_quote_volume_30d": volume_data["quote_volume"],
528580
"drift_spot_base_volume_30d": volume_data["base_volume"],
@@ -532,13 +584,13 @@ def calculate_market_volume(symbol: str) -> dict:
532584
total_base_volume += volume_data["base_volume"]
533585

534586
# Update total volumes
535-
drift_markets[symbol]["drift_total_quote_volume_30d"] = total_quote_volume
536-
drift_markets[symbol]["drift_total_base_volume_30d"] = total_base_volume
587+
drift_markets[normalized_symbol]["drift_total_quote_volume_30d"] = total_quote_volume
588+
drift_markets[normalized_symbol]["drift_total_base_volume_30d"] = total_base_volume
537589

538590
# Update OI and funding rate if perp markets exist
539591
if market_info.get("drift_is_listed_perp") == "true":
540-
drift_markets[symbol]["drift_open_interest"] = total_quote_volume * 0.75 # Example calculation
541-
drift_markets[symbol]["drift_funding_rate_1h"] = market_info.get("drift_funding_rate_1h", 0.0)
592+
drift_markets[normalized_symbol]["drift_open_interest"] = total_quote_volume * 0.75 # Example calculation
593+
drift_markets[normalized_symbol]["drift_funding_rate_1h"] = market_info.get("drift_funding_rate_1h", 0.0)
542594

543595
except Exception as e:
544596
logger.error(f"Error in fetch_drift_data_api_data: {e}")
@@ -818,8 +870,11 @@ def process_drift_markets(scored_data: List[Dict], drift_data: Dict) -> Dict:
818870
if not symbol:
819871
continue
820872

873+
# Clean and normalize the symbol to handle basket markets
874+
normalized_symbol = clean_market_name(symbol).upper()
875+
821876
# Initialize default structure for the symbol
822-
processed_drift_data[symbol] = {
877+
processed_drift_data[normalized_symbol] = {
823878
"drift_is_listed_perp": symbol_drift_data.get('drift_is_listed_perp', 'false'),
824879
"drift_is_listed_spot": symbol_drift_data.get('drift_is_listed_spot', 'false'),
825880
"drift_perp_markets": symbol_drift_data.get('drift_perp_markets', {}),
@@ -836,7 +891,7 @@ def process_drift_markets(scored_data: List[Dict], drift_data: Dict) -> Dict:
836891
total_base_volume = 0.0
837892

838893
# Sum volumes from perp markets
839-
for market_name, market_data in processed_drift_data[symbol].get("drift_perp_markets", {}).items():
894+
for market_name, market_data in processed_drift_data[normalized_symbol].get("drift_perp_markets", {}).items():
840895
total_quote_volume += market_data.get("drift_perp_quote_volume_30d", 0.0)
841896
total_base_volume += market_data.get("drift_perp_base_volume_30d", 0.0)
842897
# Clean up redundant/old fields if they exist
@@ -846,24 +901,23 @@ def process_drift_markets(scored_data: List[Dict], drift_data: Dict) -> Dict:
846901
del market_data["drift_perp_oi"]
847902

848903
# Sum volumes from spot markets
849-
for market_name, market_data in processed_drift_data[symbol].get("drift_spot_markets", {}).items():
904+
for market_name, market_data in processed_drift_data[normalized_symbol].get("drift_spot_markets", {}).items():
850905
total_quote_volume += market_data.get("drift_spot_quote_volume_30d", 0.0)
851906
total_base_volume += market_data.get("drift_spot_base_volume_30d", 0.0)
852907
# Clean up redundant/old fields if they exist
853908
if "drift_spot_volume_30d" in market_data:
854909
del market_data["drift_spot_volume_30d"]
855910

856911
# Update total volumes at the symbol level
857-
processed_drift_data[symbol]["drift_total_quote_volume_30d"] = total_quote_volume
858-
processed_drift_data[symbol]["drift_total_base_volume_30d"] = total_base_volume
912+
processed_drift_data[normalized_symbol]["drift_total_quote_volume_30d"] = total_quote_volume
913+
processed_drift_data[normalized_symbol]["drift_total_base_volume_30d"] = total_base_volume
859914

860915
# Ensure OI and funding rate are correctly placed (usually associated with perps)
861916
# If no perps listed, these should likely be 0 or handled appropriately
862-
if processed_drift_data[symbol]["drift_is_listed_perp"] == 'false':
863-
processed_drift_data[symbol]["drift_open_interest"] = 0.0
864-
processed_drift_data[symbol]["drift_funding_rate_1h"] = 0.0
865-
processed_drift_data[symbol]["drift_max_leverage"] = 0.0 # Max leverage applies to perps
866-
917+
if processed_drift_data[normalized_symbol]["drift_is_listed_perp"] == 'false':
918+
processed_drift_data[normalized_symbol]["drift_open_interest"] = 0.0
919+
processed_drift_data[normalized_symbol]["drift_funding_rate_1h"] = 0.0
920+
processed_drift_data[normalized_symbol]["drift_max_leverage"] = 0.0 # Max leverage applies to perps
867921

868922
return processed_drift_data
869923

0 commit comments

Comments
 (0)