""" Bitget UTA 数据服务 - 获取加密货币 K 线数据和技术指标 使用 requests 直接调用 REST API """ import pandas as pd import numpy as np import requests from typing import Dict, List, Optional, Any from app.utils.logger import logger class BitgetService: """Bitget UTA 数据服务(使用 requests 直接调用 REST API)""" # K线周期映射 - 注意 Bitget 使用大写 H INTERVALS = { '1m': '1m', '5m': '5m', '15m': '15m', '30m': '30m', '1h': '1H', # Bitget 大写 } # Bitget API 基础 URL BASE_URL = "https://api.bitget.com" TESTNET_URL = "https://api-testnet.bitget.com" # 产品类型 CATEGORY_SPOT = 'SPOT' CATEGORY_USDT_FUTURES = 'USDT-FUTURES' CATEGORY_COIN_FUTURES = 'COIN-FUTURES' CATEGORY_USDC_FUTURES = 'USDC-FUTURES' def __init__(self, api_key: str = "", api_secret: str = "", use_testnet: bool = False): """ 初始化 Bitget 服务 Args: api_key: API 密钥(可选,公开数据不需要) api_secret: API 密钥(可选) use_testnet: 是否使用测试网 """ self._api_key = api_key self._api_secret = api_secret self._base_url = self.TESTNET_URL if use_testnet else self.BASE_URL self._session = requests.Session() if api_key: self._session.headers.update({ 'ACCESS-KEY': api_key, 'ACCESS-SIGN': api_secret }) logger.info(f"Bitget 服务初始化完成 ({'测试网' if use_testnet else '生产网'})") def get_klines(self, symbol: str, interval: str, limit: int = 100, category: str = None) -> pd.DataFrame: """ 获取 K 线数据 Args: symbol: 交易对,如 'BTCUSDT' interval: K线周期,如 '5m', '15m', '1h', '4h' limit: 获取数量(最大1000) category: 产品类型,默认 USDT-FUTURES Returns: DataFrame 包含 OHLCV 数据 """ try: if category is None: category = self.CATEGORY_USDT_FUTURES bitget_interval = self.INTERVALS.get(interval, interval) url = f"{self._base_url}/api/v3/market/candles" params = { 'category': category, 'symbol': symbol, 'interval': bitget_interval, 'limit': str(min(limit, 1000)) # Bitget 最大 1000 } response = self._session.get(url, params=params, timeout=10) response.raise_for_status() result = response.json() # Bitget 返回格式: {"code": "00000", "msg": "success", "requestTime": ..., "data": [...]} if result.get('code') != '00000': logger.error(f"Bitget API 错误: {result.get('msg')}") return pd.DataFrame() klines = result.get('data', []) return self._parse_klines(klines) except Exception as e: logger.error(f"获取 {symbol} {interval} K线数据失败: {e}") return pd.DataFrame() def get_multi_timeframe_data(self, symbol: str, category: str = None) -> Dict[str, pd.DataFrame]: """ 获取多周期 K 线数据 Args: symbol: 交易对 category: 产品类型,默认 USDT-FUTURES Returns: 包含 1m, 5m, 15m, 30m, 1h 数据的字典 """ # 不同周期使用不同的数据量,平衡分析深度和性能 # 1m: 200根 = 3.3小时(超短线精确入场) # 5m: 200根 = 16.7小时(日内分析) # 15m: 200根 = 2.1天(短线分析) # 30m: 200根 = 4.2天(日内趋势) # 1h: 300根 = 12.5天(日内主趋势) limits = { '1m': 200, '5m': 200, '15m': 200, '30m': 200, '1h': 300 } data = {} for interval in ['1m', '5m', '15m', '30m', '1h']: df = self.get_klines(symbol, interval, limit=limits.get(interval, 100), category=category) if not df.empty: df = self.calculate_indicators(df, interval) data[interval] = df logger.info(f"获取 {symbol} 多周期数据完成") return data def _parse_klines(self, klines: List) -> pd.DataFrame: """ 解析 K 线数据为 DataFrame Bitget 返回格式: [timestamp, open, high, low, close, base_volume, quote_volume] """ if not klines: return pd.DataFrame() df = pd.DataFrame(klines, columns=[ 'open_time', 'open', 'high', 'low', 'close', 'base_volume', 'quote_volume' ]) # 转换数据类型 df['open_time'] = pd.to_datetime(df['open_time'].astype(int), unit='ms') for col in ['open', 'high', 'low', 'close', 'base_volume', 'quote_volume']: df[col] = df[col].astype(float) # 重命名以匹配 Binance 格式 df = df.rename(columns={'base_volume': 'volume'}) # 只保留需要的列(与 Binance 保持一致) df = df[['open_time', 'open', 'high', 'low', 'close', 'volume']] # 按时间排序(Bitget 返回的数据可能是倒序) df = df.sort_values('open_time').reset_index(drop=True) return df def calculate_indicators(self, df: pd.DataFrame, interval: str = '1h') -> pd.DataFrame: """ 计算技术指标 Args: df: K线数据 DataFrame interval: K线周期,用于调整 MA 参数 Returns: 添加了技术指标的 DataFrame """ if df.empty: return df # 根据周期调整 MA 参数 ma_config = { '1m': {'ma_short': 5, 'ma_mid': 10, 'ma_long': 20, 'ma_extra': 50}, '5m': {'ma_short': 5, 'ma_mid': 10, 'ma_long': 20, 'ma_extra': 50}, '15m': {'ma_short': 5, 'ma_mid': 10, 'ma_long': 20, 'ma_extra': 50}, '30m': {'ma_short': 5, 'ma_mid': 10, 'ma_long': 20, 'ma_extra': 50}, '1h': {'ma_short': 5, 'ma_mid': 10, 'ma_long': 20, 'ma_extra': 50}, } config = ma_config.get(interval, ma_config['1h']) # EMA(日内交易使用 EMA 反应更快) df['ema5'] = self._calculate_ema(df['close'], config['ma_short']) df['ema10'] = self._calculate_ema(df['close'], config['ma_mid']) df['ema20'] = self._calculate_ema(df['close'], config['ma_long']) df['ema50'] = self._calculate_ema(df['close'], config['ma_extra']) # 兼容:保留 ma5/ma10/ma20/ma50 作为 EMA 的别名 df['ma5'] = df['ema5'] df['ma10'] = df['ema10'] df['ma20'] = df['ema20'] df['ma50'] = df['ema50'] # MACD EMA(保持不变) df['ema12'] = self._calculate_ema(df['close'], 12) df['ema26'] = self._calculate_ema(df['close'], 26) # RSI df['rsi'] = self._calculate_rsi(df['close'], 14) # MACD df['macd'], df['macd_signal'], df['macd_hist'] = self._calculate_macd(df['close']) # 布林带 df['bb_upper'], df['bb_middle'], df['bb_lower'] = self._calculate_bollinger(df['close']) # KDJ df['k'], df['d'], df['j'] = self._calculate_kdj(df['high'], df['low'], df['close']) # ATR df['atr'] = self._calculate_atr(df['high'], df['low'], df['close']) # 成交量均线 df['volume_ma5'] = self._calculate_ma(df['volume'], 5) df['volume_ma20'] = self._calculate_ma(df['volume'], 20) df['volume_ratio'] = df['volume'] / df['volume_ma20'] return df @staticmethod def _calculate_ma(data: pd.Series, period: int) -> pd.Series: """简单移动平均线""" return data.rolling(window=period).mean() @staticmethod def _calculate_ema(data: pd.Series, period: int) -> pd.Series: """指数移动平均线""" return data.ewm(span=period, adjust=False).mean() @staticmethod def _calculate_rsi(data: pd.Series, period: int = 14) -> pd.Series: """RSI 指标 - 使用 Wilder's Smoothing 方法""" delta = data.diff() gain = delta.where(delta > 0, 0) loss = -delta.where(delta < 0, 0) avg_gain = gain.ewm(alpha=1/period, adjust=False).mean() avg_loss = loss.ewm(alpha=1/period, adjust=False).mean() rs = avg_gain / avg_loss rsi = 100 - (100 / (1 + rs)) return rsi @staticmethod def _calculate_macd(data: pd.Series, fast: int = 12, slow: int = 26, signal: int = 9): """MACD 指标""" ema_fast = data.ewm(span=fast, adjust=False).mean() ema_slow = data.ewm(span=slow, adjust=False).mean() macd = ema_fast - ema_slow signal_line = macd.ewm(span=signal, adjust=False).mean() histogram = macd - signal_line return macd, signal_line, histogram @staticmethod def _calculate_bollinger(data: pd.Series, period: int = 20, std_dev: float = 2.0): """布林带""" middle = data.rolling(window=period).mean() std = data.rolling(window=period).std() upper = middle + (std * std_dev) lower = middle - (std * std_dev) return upper, middle, lower @staticmethod def _calculate_kdj(high: pd.Series, low: pd.Series, close: pd.Series, period: int = 9, k_period: int = 3, d_period: int = 3): """KDJ 指标""" low_min = low.rolling(window=period).min() high_max = high.rolling(window=period).max() rsv = (close - low_min) / (high_max - low_min) * 100 k = rsv.ewm(com=k_period - 1, adjust=False).mean() d = k.ewm(com=d_period - 1, adjust=False).mean() j = 3 * k - 2 * d return k, d, j @staticmethod def _calculate_atr(high: pd.Series, low: pd.Series, close: pd.Series, period: int = 14): """ATR 平均真实波幅""" tr1 = high - low tr2 = abs(high - close.shift()) tr3 = abs(low - close.shift()) tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1) atr = tr.rolling(window=period).mean() return atr def get_current_price(self, symbol: str, category: str = None) -> Optional[float]: """ 获取当前价格 Args: symbol: 交易对 category: 产品类型,默认 USDT-FUTURES Returns: 当前价格 """ try: if category is None: category = self.CATEGORY_USDT_FUTURES url = f"{self._base_url}/api/v3/market/tickers" params = { 'category': category, 'symbol': symbol } response = self._session.get(url, params=params, timeout=10) response.raise_for_status() result = response.json() if result.get('code') != '00000': logger.error(f"Bitget API 错误: {result.get('msg')}") return None data = result.get('data', []) if not data: return None # 返回第一个(也是唯一的)ticker 的最新价格 return float(data[0].get('lastPrice', 0)) except Exception as e: logger.error(f"获取 {symbol} 当前价格失败: {e}") return None def get_ticker(self, symbol: str, category: str = None) -> Optional[Dict[str, Any]]: """ 获取完整的 Ticker 数据 Args: symbol: 交易对 category: 产品类型,默认 USDT-FUTURES Returns: 完整的 ticker 数据 """ try: if category is None: category = self.CATEGORY_USDT_FUTURES url = f"{self._base_url}/api/v3/market/tickers" params = { 'category': category, 'symbol': symbol } response = self._session.get(url, params=params, timeout=10) response.raise_for_status() result = response.json() if result.get('code') != '00000': logger.error(f"Bitget API 错误: {result.get('msg')}") return None data = result.get('data', []) if not data: return None return data[0] except Exception as e: logger.error(f"获取 {symbol} ticker 失败: {e}") return None def get_symbol_info(self, symbol: str, category: str = None) -> Optional[Dict[str, Any]]: """ 获取交易对信息(包含精度配置) Args: symbol: 交易对 category: 产品类型,默认 USDT-FUTURES Returns: 交易对信息,包含 pricePrecision 和 quantityPrecision """ try: if category is None: category = self.CATEGORY_USDT_FUTURES url = f"{self._base_url}/api/v3/market/symbols" params = { 'category': category, 'symbol': symbol } response = self._session.get(url, params=params, timeout=10) response.raise_for_status() result = response.json() if result.get('code') != '00000': logger.error(f"Bitget API 错误: {result.get('msg')}") return None data = result.get('data', []) if not data: return None symbol_data = data[0] # 返回精度信息 return { 'symbol': symbol_data.get('symbol'), 'status': symbol_data.get('status'), 'pricePrecision': int(symbol_data.get('pricePrecision', 2)), 'quantityPrecision': int(symbol_data.get('quantityPrecision', 2)), 'minTradeAmount': float(symbol_data.get('minTradeAmount', 0)), 'maxTradeAmount': float(symbol_data.get('maxTradeAmount', 0)), 'takerFeeRate': float(symbol_data.get('takerFeeRate', 0.001)), 'makerFeeRate': float(symbol_data.get('makerFeeRate', 0.001)), } except Exception as e: logger.error(f"获取 {symbol} 交易对信息失败: {e}") return None # 缓存交易对精度信息 _symbol_precision_cache: Dict[str, Dict[str, int]] = {} def get_precision(self, symbol: str, category: str = None) -> Dict[str, int]: """ 获取交易对价格和数量精度(带缓存) Args: symbol: 交易对 category: 产品类型,默认 USDT-FUTURES Returns: {'pricePrecision': 2, 'quantityPrecision': 2} """ # 检查缓存 if symbol in self._symbol_precision_cache: return self._symbol_precision_cache[symbol] # 获取交易对信息 info = self.get_symbol_info(symbol, category) if info: precision = { 'pricePrecision': info['pricePrecision'], 'quantityPrecision': info['quantityPrecision'] } # 缓存结果 self._symbol_precision_cache[symbol] = precision return precision # 默认精度 return {'pricePrecision': 2, 'quantityPrecision': 2} def round_price(self, symbol: str, price: float, category: str = None) -> float: """ 根据交易对精度四舍五入价格 Args: symbol: 交易对 price: 原始价格 category: 产品类型 Returns: 四舍五入后的价格 """ precision = self.get_precision(symbol, category) return round(price, precision['pricePrecision']) def round_quantity(self, symbol: str, quantity: float, category: str = None) -> float: """ 根据交易对精度四舍五入数量 Args: symbol: 交易对 quantity: 原始数量 category: 产品类型 Returns: 四舍五入后的数量 """ precision = self.get_precision(symbol, category) return round(quantity, precision['quantityPrecision']) def get_funding_rate(self, symbol: str) -> Optional[Dict[str, Any]]: """ 获取资金费率(包含标记价格和指数价格) Args: symbol: 交易对,如 'BTCUSDT' Returns: 包含资金费率、标记价格、指数价格的字典 """ try: # 同时获取 ticker 数据(包含标记价格和指数价格) ticker = self.get_ticker(symbol, self.CATEGORY_USDT_FUTURES) if not ticker: logger.error(f"获取 {symbol} ticker 数据失败") return None # 获取资金费率 url = f"{self._base_url}/api/v3/market/current-fund-rate" params = {'symbol': symbol} response = self._session.get(url, params=params, timeout=10) response.raise_for_status() result = response.json() if result.get('code') != '00000': logger.error(f"Bitget API 错误: {result.get('msg')}") return None data = result.get('data', []) if not data: return None funding_data = data[0] # 解析资金费率 funding_rate = float(funding_data.get('fundingRate', 0)) next_update = int(funding_data.get('nextUpdate', 0)) min_rate = float(funding_data.get('minFundingRate', 0)) max_rate = float(funding_data.get('maxFundingRate', 0)) # 从 ticker 获取标记价格和指数价格 mark_price = float(ticker.get('markPrice', 0)) index_price = float(ticker.get('indexPrice', 0)) # 判断市场情绪 if funding_rate > 0.01: # > 0.1% sentiment = "极度贪婪" sentiment_level = "extreme_greed" elif funding_rate > 0.0005: # > 0.05% sentiment = "贪婪" sentiment_level = "greed" elif funding_rate < -0.01: # < -0.1% sentiment = "极度恐惧" sentiment_level = "extreme_fear" elif funding_rate < -0.0005: # < -0.05% sentiment = "恐惧" sentiment_level = "fear" else: sentiment = "中性" sentiment_level = "neutral" return { 'funding_rate': funding_rate, 'funding_rate_percent': funding_rate * 100, 'next_funding_time': next_update, 'min_funding_rate': min_rate, 'max_funding_rate': max_rate, 'mark_price': mark_price, 'index_price': index_price, 'sentiment': sentiment, 'sentiment_level': sentiment_level } except Exception as e: logger.error(f"获取 {symbol} 资金费率失败: {e}") return None def get_open_interest(self, symbol: str) -> Optional[Dict[str, Any]]: """ 获取持仓量 Args: symbol: 交易对,如 'BTCUSDT' Returns: 包含持仓量信息的字典 """ try: # 从 ticker 获取持仓量 ticker = self.get_ticker(symbol, self.CATEGORY_USDT_FUTURES) if not ticker: return None open_interest = float(ticker.get('openInterest', 0)) return { 'open_interest': open_interest, 'timestamp': int(ticker.get('ts', 0)) } except Exception as e: logger.error(f"获取 {symbol} 持仓量失败: {e}") return None def get_futures_market_data(self, symbol: str) -> Optional[Dict[str, Any]]: """ 获取合约市场综合数据(资金费率 + 持仓量) Args: symbol: 交易对 Returns: 综合市场数据 """ try: # 获取数据 funding_rate = self.get_funding_rate(symbol) open_interest = self.get_open_interest(symbol) ticker = self.get_ticker(symbol, self.CATEGORY_USDT_FUTURES) if not funding_rate or not open_interest or not ticker: logger.warning(f"获取 {symbol} 合约数据不完整") return None # 计算溢价率 premium_rate = 0 index_price = float(ticker.get('indexPrice', 0)) mark_price = float(ticker.get('markPrice', 0)) if index_price > 0: premium_rate = ((mark_price - index_price) / index_price * 100) return { 'funding_rate': funding_rate, 'open_interest': open_interest, 'premium_rate': premium_rate, 'market_sentiment': funding_rate.get('sentiment', ''), 'sentiment_level': funding_rate.get('sentiment_level', ''), 'mark_price': mark_price, 'index_price': index_price } except Exception as e: logger.error(f"获取 {symbol} 合约市场数据失败: {e}") return None def format_futures_data_for_llm(self, symbol: str, market_data: Dict[str, Any]) -> str: """ 格式化合约数据供 LLM 分析 Args: symbol: 交易对 market_data: 合约市场数据 Returns: 格式化的文本 """ if not market_data: return "" lines = [f"\n## {symbol} 合约市场数据\n"] # 资金费率 funding = market_data.get('funding_rate', {}) if funding: fr = funding.get('funding_rate_percent', 0) sentiment = funding.get('sentiment', '') lines.append(f"### 资金费率") lines.append(f"• 当前费率: {fr:.4f}%") lines.append(f"• 市场情绪: {sentiment}") lines.append(f"• 标记价格: ${market_data.get('mark_price', 0):,.2f}") lines.append(f"• 指数价格: ${market_data.get('index_price', 0):,.2f}") # 资金费率分析 if fr > 0.1: lines.append(f"• ⚠️ 极高费率,多头过度杠杆,警惕回调风险") elif fr > 0.05: lines.append(f"• 正费率,多头占优但未极端") elif fr < -0.1: lines.append(f"• ⚠️ 极低费率,空头过度杠杆,可能反弹") elif fr < -0.05: lines.append(f"• 负费率,空头占优但未极端") # 持仓量 oi = market_data.get('open_interest', {}) if oi: lines.append(f"\n### 持仓量") lines.append(f"• 当前持仓: {oi.get('open_interest', 0):,.0f} 张") # 溢价率 premium = market_data.get('premium_rate', 0) if premium != 0: lines.append(f"\n### 溢价分析") lines.append(f"• 现货溢价: {premium:+.2f}%") if premium > 1: lines.append(f"• ⚠️ 高溢价,市场过热") elif premium < -1: lines.append(f"• ⚠️ 负溢价,市场偏冷") return "\n".join(lines) # 全局实例 bitget_service = BitgetService()