1
1
# 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.
2
2
3
+ import os
4
+ import time
3
5
import logging
4
- from typing import Dict , List
6
+ from datetime import datetime , timedelta
7
+ from typing import Dict , List , Optional , Tuple , Any
5
8
from fastapi import APIRouter
6
- from backend .utils .coingecko_api import fetch_all_coingecko_market_data , fetch_all_coingecko_volumes
7
9
from driftpy .pickle .vat import Vat
8
10
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
+ )
11
20
12
21
# --- Constants ---
13
22
PRICE_PRECISION = 1e6 # Add price precision constant
16
25
}
17
26
18
27
# 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
+ ]
20
54
21
55
# Symbol conversions
22
56
SYMBOL_CONVERSIONS = {
@@ -47,30 +81,34 @@ async def get_market_data(request: BackendRequest, number_of_tokens: int = 2):
47
81
"""
48
82
return main (request .state .backend_state .vat , number_of_tokens )
49
83
84
+ @ttl_cache (ttl_seconds = 43200 ) # Cache for 12 hours
50
85
def fetch_coingecko_market_data (number_of_tokens : int = 2 ) -> List [Dict ]:
51
86
"""
52
87
Fetches market data for top N coins from CoinGecko API.
88
+ Responses are cached for 12 hours.
53
89
54
90
Args:
55
91
number_of_tokens (int, optional): Number of tokens to fetch data for. Defaults to 2.
56
92
57
93
Returns:
58
94
List of dictionaries containing market data for each coin
59
95
"""
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..." )
61
97
62
98
try :
63
- # Fetch market data
99
+ # Fetch market data (this call is now cached)
100
+ logger .info ("📊 Fetching CoinGecko market data..." )
64
101
market_data = fetch_all_coingecko_market_data (number_of_tokens )
65
102
66
103
if not market_data :
67
- logger .error ("Failed to fetch market data from CoinGecko" )
104
+ logger .error ("❌ Failed to fetch market data from CoinGecko" )
68
105
return []
69
106
70
107
# Get list of coin IDs for volume fetching
71
108
coin_ids = list (market_data .keys ())
109
+ logger .info (f"📈 Found { len (coin_ids )} coins, fetching volume data..." )
72
110
73
- # Fetch 30-day volume data
111
+ # Fetch 30-day volume data (this call is now cached)
74
112
volume_data = fetch_all_coingecko_volumes (coin_ids )
75
113
76
114
# Format response data
@@ -97,16 +135,16 @@ def fetch_coingecko_market_data(number_of_tokens: int = 2) -> List[Dict]:
97
135
}
98
136
}
99
137
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' ]} " )
101
139
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 } " )
103
141
continue
104
142
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" )
106
144
return formatted_data
107
145
108
146
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 )
110
148
return []
111
149
112
150
def fetch_driftpy_data (vat : Vat ) -> Dict :
@@ -173,17 +211,22 @@ def fetch_driftpy_data(vat: Vat) -> Dict:
173
211
try :
174
212
# Get market name and clean it
175
213
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
+
176
219
# Preserve original case for API calls
177
220
original_market_name = market_name
178
221
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"
181
224
symbol_parts = market_name .split ('-' )
182
225
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 ()
185
228
else :
186
- normalized_symbol = market_name . upper (). strip ()
229
+ normalized_symbol = clean_market_name ( market_name ). upper ()
187
230
188
231
# Get oracle price from perp_oracles
189
232
market_price = vat .perp_oracles .get (market .data .market_index )
@@ -382,13 +425,13 @@ def fetch_api_page(url: str, retries: int = 5) -> Dict:
382
425
383
426
return {"success" : False , "records" : [], "meta" : {"totalRecords" : 0 }}
384
427
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 ]:
386
429
"""
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 .
389
432
390
433
Args:
391
- symbol: Market symbol (e.g., "SOL-PERP")
434
+ market_name: Original market name (e.g., "1MPEPE-PERP" , "SOL-PERP")
392
435
start_date: Start date for candle data
393
436
end_date: End date for candle data (defaults to current time)
394
437
@@ -398,8 +441,8 @@ def fetch_market_trades(symbol: str, start_date: datetime, end_date: datetime =
398
441
if end_date is None :
399
442
end_date = datetime .now ()
400
443
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 } " )
403
446
404
447
# Convert dates to Unix timestamps (seconds)
405
448
# 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 =
416
459
417
460
# Use the candles endpoint with daily resolution (D)
418
461
# 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 )} "
420
463
421
464
logger .info (f"Requesting candles with startTs={ start_ts } (now), endTs={ end_ts } (past)" )
422
465
@@ -425,19 +468,19 @@ def fetch_market_trades(symbol: str, start_date: datetime, end_date: datetime =
425
468
426
469
if data .get ("success" ) and "records" in data :
427
470
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 } " )
429
472
return data ["records" ]
430
473
else :
431
- logger .warning (f"Failed to fetch candle data for { symbol } " )
474
+ logger .warning (f"Failed to fetch candle data for { market_name } " )
432
475
return []
433
476
434
- def calculate_market_volume (symbol : str ) -> dict :
477
+ def calculate_market_volume (market_name : str ) -> dict :
435
478
"""
436
479
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 .
438
481
439
482
Args:
440
- symbol: Market symbol (e.g., "SOL-PERP")
483
+ market_name: Original market name (e.g., "1MPEPE-PERP" , "SOL-PERP")
441
484
442
485
Returns:
443
486
dict: Dictionary with quote_volume and base_volume
@@ -446,7 +489,7 @@ def calculate_market_volume(symbol: str) -> dict:
446
489
start_date = end_date - timedelta (days = DAYS_TO_CONSIDER )
447
490
448
491
try :
449
- candles = fetch_market_trades (symbol , start_date , end_date )
492
+ candles = fetch_market_trades (market_name , start_date , end_date )
450
493
451
494
total_quote_volume = 0.0
452
495
total_base_volume = 0.0
@@ -462,7 +505,7 @@ def calculate_market_volume(symbol: str) -> dict:
462
505
except (ValueError , TypeError ):
463
506
continue
464
507
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 } : "
466
509
f"${ total_quote_volume :,.2f} (quote), { total_base_volume :,.2f} (base)" )
467
510
468
511
return {
@@ -471,7 +514,7 @@ def calculate_market_volume(symbol: str) -> dict:
471
514
}
472
515
473
516
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 )} " )
475
518
return {
476
519
"quote_volume" : 0.0 ,
477
520
"base_volume" : 0.0
@@ -486,7 +529,10 @@ def calculate_market_volume(symbol: str) -> dict:
486
529
logger .info (f"Processing { len (discovered_markets )} discovered markets" )
487
530
488
531
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 ] = {
490
536
"drift_is_listed_spot" : market_info .get ("drift_is_listed_spot" , "false" ),
491
537
"drift_is_listed_perp" : market_info .get ("drift_is_listed_perp" , "false" ),
492
538
"drift_perp_markets" : {},
@@ -502,11 +548,17 @@ def calculate_market_volume(symbol: str) -> dict:
502
548
total_quote_volume = 0.0
503
549
total_base_volume = 0.0
504
550
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
+
505
557
# Use original market name (case-preserved) for API call
506
558
logger .info (f"Calculating volume for perp market: { perp_market_name } (preserving case)" )
507
559
volume_data = calculate_market_volume (perp_market_name ) # Returns dict with quote and base volumes
508
560
509
- drift_markets [symbol ]["drift_perp_markets" ][perp_market_name ] = {
561
+ drift_markets [normalized_symbol ]["drift_perp_markets" ][perp_market_name ] = {
510
562
"drift_perp_oracle_price" : market_info ["drift_perp_markets" ][perp_market_name ].get ("drift_perp_oracle_price" , 0.0 ),
511
563
"drift_perp_quote_volume_30d" : volume_data ["quote_volume" ],
512
564
"drift_perp_base_volume_30d" : volume_data ["base_volume" ],
@@ -522,7 +574,7 @@ def calculate_market_volume(symbol: str) -> dict:
522
574
logger .info (f"Calculating volume for spot market: { spot_market_name } (preserving case)" )
523
575
volume_data = calculate_market_volume (spot_market_name ) # Returns dict with quote and base volumes
524
576
525
- drift_markets [symbol ]["drift_spot_markets" ][spot_market_name ] = {
577
+ drift_markets [normalized_symbol ]["drift_spot_markets" ][spot_market_name ] = {
526
578
"drift_spot_oracle_price" : market_info ["drift_spot_markets" ][spot_market_name ].get ("drift_spot_oracle_price" , 0.0 ),
527
579
"drift_spot_quote_volume_30d" : volume_data ["quote_volume" ],
528
580
"drift_spot_base_volume_30d" : volume_data ["base_volume" ],
@@ -532,13 +584,13 @@ def calculate_market_volume(symbol: str) -> dict:
532
584
total_base_volume += volume_data ["base_volume" ]
533
585
534
586
# 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
537
589
538
590
# Update OI and funding rate if perp markets exist
539
591
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 )
542
594
543
595
except Exception as e :
544
596
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:
818
870
if not symbol :
819
871
continue
820
872
873
+ # Clean and normalize the symbol to handle basket markets
874
+ normalized_symbol = clean_market_name (symbol ).upper ()
875
+
821
876
# Initialize default structure for the symbol
822
- processed_drift_data [symbol ] = {
877
+ processed_drift_data [normalized_symbol ] = {
823
878
"drift_is_listed_perp" : symbol_drift_data .get ('drift_is_listed_perp' , 'false' ),
824
879
"drift_is_listed_spot" : symbol_drift_data .get ('drift_is_listed_spot' , 'false' ),
825
880
"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:
836
891
total_base_volume = 0.0
837
892
838
893
# 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 ():
840
895
total_quote_volume += market_data .get ("drift_perp_quote_volume_30d" , 0.0 )
841
896
total_base_volume += market_data .get ("drift_perp_base_volume_30d" , 0.0 )
842
897
# Clean up redundant/old fields if they exist
@@ -846,24 +901,23 @@ def process_drift_markets(scored_data: List[Dict], drift_data: Dict) -> Dict:
846
901
del market_data ["drift_perp_oi" ]
847
902
848
903
# 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 ():
850
905
total_quote_volume += market_data .get ("drift_spot_quote_volume_30d" , 0.0 )
851
906
total_base_volume += market_data .get ("drift_spot_base_volume_30d" , 0.0 )
852
907
# Clean up redundant/old fields if they exist
853
908
if "drift_spot_volume_30d" in market_data :
854
909
del market_data ["drift_spot_volume_30d" ]
855
910
856
911
# 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
859
914
860
915
# Ensure OI and funding rate are correctly placed (usually associated with perps)
861
916
# 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
867
921
868
922
return processed_drift_data
869
923
0 commit comments