Skip to content

Commit 1e7bfd9

Browse files
committed
Enhance Market Recommender and Introduce Caching Mechanism
- Updated the market recommender logic to utilize the CoinGecko API more effectively, including new constants for ignored symbols and improved data handling. - Implemented a caching mechanism for API calls using a TTL cache, significantly reducing redundant requests and improving performance. - Enhanced logging throughout the market data fetching process, providing clearer insights into cache hits, misses, and data processing. - Added new utility functions for cleaning market names and filtering out ignored symbols, ensuring more accurate data representation. - Introduced a new test suite for the caching functionality, validating the effectiveness of the caching mechanism and ensuring consistent results across API calls. - Refined the Streamlit frontend to improve user experience with additional filtering options and clearer display of market recommendations. These changes aim to optimize the market recommender's performance and reliability while enhancing the overall user experience.
1 parent bc16cd2 commit 1e7bfd9

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)