Signed-off-by: TIANHE <TIANHE@GMAIL.COM>
This commit is contained in:
TIANHE
2026-02-28 20:34:33 +08:00
parent 3db3bb2a9a
commit 7c067fe61c
7 changed files with 1346 additions and 67 deletions
+186 -20
View File
@@ -2,7 +2,7 @@
加密货币数据源
使用 CCXT (Coinbase) 获取数据
"""
from typing import Dict, List, Any, Optional
from typing import Dict, List, Any, Optional, Tuple
from datetime import datetime, timedelta
import ccxt
@@ -21,6 +21,9 @@ class CryptoDataSource(BaseDataSource):
# 时间周期映射
TIMEFRAME_MAP = CCXTConfig.TIMEFRAME_MAP
# 常见的报价货币列表(按优先级排序)
COMMON_QUOTES = ['USDT', 'USD', 'BTC', 'ETH', 'BUSD', 'USDC', 'BNB', 'EUR', 'GBP']
def __init__(self):
config = {
'timeout': CCXTConfig.TIMEOUT,
@@ -43,27 +46,189 @@ class CryptoDataSource(BaseDataSource):
exchange_class = getattr(ccxt, exchange_id)
self.exchange = exchange_class(config)
# 延迟加载 markets(首次使用时加载)
self._markets_loaded = False
self._markets_cache = None
def _ensure_markets_loaded(self) -> bool:
"""确保 markets 已加载(用于符号验证)"""
if self._markets_loaded and self._markets_cache is not None:
return True
try:
# 某些交易所需要显式加载 markets
if hasattr(self.exchange, 'load_markets'):
self.exchange.load_markets(reload=False)
self._markets_cache = getattr(self.exchange, 'markets', {})
self._markets_loaded = True
return True
except Exception as e:
logger.debug(f"Failed to load markets for {self.exchange.id}: {e}")
return False
def _normalize_symbol(self, symbol: str) -> Tuple[str, str]:
"""
规范化符号格式,返回 (normalized_symbol, base_currency)
处理各种输入格式:
- BTC/USDT -> BTC/USDT
- BTCUSDT -> BTC/USDT
- BTC/USDT:USDT -> BTC/USDT
- BTC -> BTC/USDT (默认)
- PI, TRX -> PI/USDT, TRX/USDT
"""
if not symbol:
return '', ''
sym = symbol.strip()
# 移除 swap/futures 后缀
if ':' in sym:
sym = sym.split(':', 1)[0]
sym = sym.upper()
# 如果已经有分隔符,直接解析
if '/' in sym:
parts = sym.split('/', 1)
base = parts[0].strip()
quote = parts[1].strip() if len(parts) > 1 else ''
if base and quote:
return f"{base}/{quote}", base
# 尝试从常见报价货币中识别
for quote in self.COMMON_QUOTES:
if sym.endswith(quote) and len(sym) > len(quote):
base = sym[:-len(quote)]
if base:
return f"{base}/{quote}", base
# 如果无法识别,默认使用 USDT
return f"{sym}/USDT", sym
def _find_valid_symbol(self, base: str, preferred_quote: str = 'USDT') -> Optional[str]:
"""
在交易所的 markets 中查找有效的符号
Args:
base: 基础货币(如 'PI', 'TRX'
preferred_quote: 首选的报价货币
Returns:
找到的有效符号,如果找不到则返回 None
"""
if not self._ensure_markets_loaded():
return None
markets = self._markets_cache or {}
if not markets:
return None
# 按优先级尝试不同的报价货币
quotes_to_try = [preferred_quote] + [q for q in self.COMMON_QUOTES if q != preferred_quote]
for quote in quotes_to_try:
candidate = f"{base}/{quote}"
if candidate in markets:
market = markets[candidate]
# 检查市场是否活跃
if market.get('active', True):
return candidate
return None
def _normalize_symbol_for_exchange(self, symbol: str) -> str:
"""
根据交易所特性规范化符号
不同交易所的符号格式要求:
- Binance: BTC/USDT (标准格式)
- OKX: BTC/USDT (标准格式,但某些币种可能不支持)
- Coinbase: BTC/USD (通常使用 USD 而不是 USDT)
- Kraken: XBT/USD (BTC 映射为 XBT)
- Bitfinex: tBTCUST (特殊格式)
"""
normalized, base = self._normalize_symbol(symbol)
if not normalized or not base:
return symbol
exchange_id = getattr(self.exchange, 'id', '').lower()
# 特殊处理:某些交易所的符号映射
if exchange_id == 'coinbase':
# Coinbase 通常使用 USD 而不是 USDT
if normalized.endswith('/USDT'):
usd_version = normalized.replace('/USDT', '/USD')
if self._ensure_markets_loaded():
markets = self._markets_cache or {}
if usd_version in markets:
return usd_version
# 尝试在交易所中查找有效符号
if self._ensure_markets_loaded():
valid_symbol = self._find_valid_symbol(base, normalized.split('/')[1] if '/' in normalized else 'USDT')
if valid_symbol:
return valid_symbol
return normalized
def get_ticker(self, symbol: str) -> Dict[str, Any]:
"""
Get latest ticker for a crypto symbol via CCXT.
Accepts common formats:
- BTC/USDT
- BTCUSDT
- BTC/USDT:USDT (swap-style suffix, will be normalized)
- BTC/USDT, BTCUSDT, BTC/USDT:USDT
- PI, TRX (will be normalized and searched across exchanges)
- 自动适配不同交易所的符号格式要求
"""
sym = (symbol or "").strip()
if ":" in sym:
sym = sym.split(":", 1)[0]
sym = sym.upper()
if "/" not in sym:
# Coinbase often uses USD, check if we need to adapt
if sym.endswith("USDT") and len(sym) > 4:
sym = f"{sym[:-4]}/USDT"
elif sym.endswith("USD") and len(sym) > 3:
sym = f"{sym[:-3]}/USD"
return self.exchange.fetch_ticker(sym)
if not symbol or not symbol.strip():
return {'last': 0, 'symbol': symbol}
# 规范化符号
normalized = self._normalize_symbol_for_exchange(symbol)
if not normalized:
logger.warning(f"Failed to normalize symbol: {symbol}")
return {'last': 0, 'symbol': symbol}
# 尝试获取 ticker
try:
ticker = self.exchange.fetch_ticker(normalized)
if ticker and isinstance(ticker, dict):
return ticker
except Exception as e:
error_msg = str(e).lower()
is_symbol_error = any(keyword in error_msg for keyword in [
'does not have market symbol',
'symbol not found',
'invalid symbol',
'market does not exist',
'trading pair not found'
])
if is_symbol_error:
# 尝试查找替代符号
base = normalized.split('/')[0] if '/' in normalized else normalized
if self._ensure_markets_loaded():
valid_symbol = self._find_valid_symbol(base)
if valid_symbol and valid_symbol != normalized:
try:
logger.debug(f"Trying alternative symbol: {valid_symbol} (original: {symbol}, first attempt: {normalized})")
ticker = self.exchange.fetch_ticker(valid_symbol)
if ticker and isinstance(ticker, dict):
return ticker
except Exception as e2:
logger.debug(f"Alternative symbol {valid_symbol} also failed: {e2}")
# 如果所有尝试都失败,记录警告并返回默认值
logger.warning(
f"Symbol '{symbol}' (normalized: {normalized}) not found on {self.exchange.id}. "
f"Error: {str(e)[:100]}"
)
return {'last': 0, 'symbol': symbol}
def get_kline(
self,
@@ -78,11 +243,12 @@ class CryptoDataSource(BaseDataSource):
try:
ccxt_timeframe = self.TIMEFRAME_MAP.get(timeframe, '1d')
# 构建交易对符号
if not symbol.endswith('USDT') and not symbol.endswith('USD'):
symbol_pair = f'{symbol}/USDT'
else:
symbol_pair = symbol
# 使用统一的符号规范化方法
symbol_pair = self._normalize_symbol_for_exchange(symbol)
if not symbol_pair:
logger.warning(f"Failed to normalize symbol for K-line: {symbol}")
return []
# logger.info(f"获取加密货币K线: {symbol_pair}, 周期: {ccxt_timeframe}, 条数: {limit}")