""" Binance 数据服务 - 获取加密货币 K 线数据和技术指标 使用 requests 直接调用 REST API,避免与 WebSocket 的事件循环冲突 """ import pandas as pd import numpy as np import requests from typing import Dict, List, Optional, Any from app.utils.logger import logger class BinanceService: """Binance 数据服务(使用 requests 直接调用 REST API)""" # K线周期映射 INTERVALS = { '5m': '5m', '15m': '15m', '1h': '1h', '4h': '4h' } # Binance API 基础 URL BASE_URL = "https://api.binance.com" FUTURES_URL = "https://fapi.binance.com" # 合约 API def __init__(self, api_key: str = "", api_secret: str = ""): """ 初始化 Binance 服务 Args: api_key: API 密钥(可选,公开数据不需要) api_secret: API 密钥(可选) """ self._api_key = api_key self._api_secret = api_secret self._session = requests.Session() if api_key: self._session.headers.update({'X-MBX-APIKEY': api_key}) logger.info("Binance 服务初始化完成") def get_klines(self, symbol: str, interval: str, limit: int = 100) -> pd.DataFrame: """ 获取 K 线数据 Args: symbol: 交易对,如 'BTCUSDT' interval: K线周期,如 '5m', '15m', '1h', '4h' limit: 获取数量 Returns: DataFrame 包含 OHLCV 数据 """ try: binance_interval = self.INTERVALS.get(interval, interval) url = f"{self.BASE_URL}/api/v3/klines" params = { 'symbol': symbol, 'interval': binance_interval, 'limit': limit } response = self._session.get(url, params=params, timeout=10) response.raise_for_status() klines = response.json() 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) -> Dict[str, pd.DataFrame]: """ 获取多周期 K 线数据 Args: symbol: 交易对 Returns: 包含 5m, 15m, 1h, 4h 数据的字典 """ # 不同周期使用不同的数据量,平衡分析深度和性能 # 5m: 200根 = 16.7小时(日内分析) # 15m: 200根 = 2.1天(短线分析) # 1h: 300根 = 12.5天(中线分析) # 4h: 200根 = 33.3天(趋势分析) limits = { '5m': 200, '15m': 200, '1h': 300, '4h': 200 } data = {} for interval in ['5m', '15m', '1h', '4h']: df = self.get_klines(symbol, interval, limit=limits.get(interval, 100)) if not df.empty: df = self.calculate_indicators(df) data[interval] = df logger.info(f"获取 {symbol} 多周期数据完成") return data def _parse_klines(self, klines: List) -> pd.DataFrame: """解析 K 线数据为 DataFrame""" if not klines: return pd.DataFrame() df = pd.DataFrame(klines, columns=[ 'open_time', 'open', 'high', 'low', 'close', 'volume', 'close_time', 'quote_volume', 'trades', 'taker_buy_base', 'taker_buy_quote', 'ignore' ]) # 转换数据类型 df['open_time'] = pd.to_datetime(df['open_time'], unit='ms') df['close_time'] = pd.to_datetime(df['close_time'], unit='ms') for col in ['open', 'high', 'low', 'close', 'volume', 'quote_volume']: df[col] = df[col].astype(float) df['trades'] = df['trades'].astype(int) # 只保留需要的列 df = df[['open_time', 'open', 'high', 'low', 'close', 'volume', 'trades']] return df def calculate_indicators(self, df: pd.DataFrame) -> pd.DataFrame: """ 计算技术指标 Args: df: K线数据 DataFrame Returns: 添加了技术指标的 DataFrame """ if df.empty: return df # 移动平均线 df['ma5'] = self._calculate_ma(df['close'], 5) df['ma10'] = self._calculate_ma(df['close'], 10) df['ma20'] = self._calculate_ma(df['close'], 20) df['ma50'] = self._calculate_ma(df['close'], 50) # 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 指标""" delta = data.diff() gain = (delta.where(delta > 0, 0)).rolling(window=period).mean() loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean() rs = gain / 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) -> Optional[float]: """获取当前价格""" try: url = f"{self.BASE_URL}/api/v3/ticker/price" params = {'symbol': symbol} response = self._session.get(url, params=params, timeout=10) response.raise_for_status() ticker = response.json() return float(ticker['price']) except Exception as e: logger.error(f"获取 {symbol} 当前价格失败: {e}") return None def get_24h_stats(self, symbol: str) -> Optional[Dict[str, Any]]: """获取 24 小时统计数据""" try: url = f"{self.BASE_URL}/api/v3/ticker/24hr" params = {'symbol': symbol} response = self._session.get(url, params=params, timeout=10) response.raise_for_status() stats = response.json() return { 'price': float(stats['lastPrice']), 'price_change': float(stats['priceChange']), 'price_change_percent': float(stats['priceChangePercent']), 'high': float(stats['highPrice']), 'low': float(stats['lowPrice']), 'volume': float(stats['volume']), 'quote_volume': float(stats['quoteVolume']) } except Exception as e: logger.error(f"获取 {symbol} 24h 统计失败: {e}") return None def get_funding_rate(self, symbol: str) -> Optional[Dict[str, Any]]: """ 获取资金费率 Args: symbol: 交易对,如 'BTCUSDT' Returns: 包含资金费率信息的字典 """ try: url = f"{self.FUTURES_URL}/fapi/v1/premiumIndex" params = {'symbol': symbol} response = self._session.get(url, params=params, timeout=10) response.raise_for_status() data = response.json() # 解析资金费率 funding_rate = float(data.get('lastFundingRate', 0)) mark_price = float(data.get('markPrice', 0)) index_price = float(data.get('indexPrice', 0)) next_funding_time = int(data.get('nextFundingTime', 0)) # 判断市场情绪 if funding_rate > 0.01: # > 0.1% sentiment = "极度贪婪" sentiment_level = "extreme_greed" elif funding_rate > 0.05: # > 0.05% sentiment = "贪婪" sentiment_level = "greed" elif funding_rate < -0.01: # < -0.1% sentiment = "极度恐惧" sentiment_level = "extreme_fear" elif funding_rate < -0.05: # < -0.05% sentiment = "恐惧" sentiment_level = "fear" else: sentiment = "中性" sentiment_level = "neutral" return { 'funding_rate': funding_rate, 'funding_rate_percent': funding_rate * 100, # 转为百分比 'mark_price': mark_price, 'index_price': index_price, 'next_funding_time': next_funding_time, '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: url = f"{self.FUTURES_URL}/fapi/v1/openInterest" params = {'symbol': symbol} response = self._session.get(url, params=params, timeout=10) response.raise_for_status() data = response.json() open_interest = float(data.get('openInterest', 0)) open_interest_value = float(data.get('openInterestValue', 0)) return { 'open_interest': open_interest, 'open_interest_value': open_interest_value, 'timestamp': int(data.get('time', 0)) } except Exception as e: logger.error(f"获取 {symbol} 持仓量失败: {e}") return None def get_open_interest_hist(self, symbol: str, period: str = '5m', limit: int = 30) -> Optional[List[Dict[str, Any]]]: """ 获取历史持仓量(用于计算变化趋势) Args: symbol: 交易对 period: 周期 (5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d) limit: 获取数量 (最大 500) Returns: 持仓量历史列表 """ try: # Binance 使用正确的 API 端点 url = f"{self.FUTURES_URL}/futures/data/openInterestHist" params = { 'symbol': symbol, 'period': period, 'limit': limit } response = self._session.get(url, params=params, timeout=10) response.raise_for_status() data = response.json() return data 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) if not funding_rate or not open_interest: logger.warning(f"获取 {symbol} 合约数据不完整") return None # 获取历史持仓量计算趋势 hist_oi = self.get_open_interest_hist(symbol, period='1h', limit=24) oi_change = 0 oi_change_percent = 0 if hist_oi and len(hist_oi) >= 2: oi_24h_ago = float(hist_oi[-1].get('sumOpenInterest', 0)) oi_now = float(hist_oi[0].get('sumOpenInterest', 0)) oi_change = oi_now - oi_24h_ago oi_change_percent = (oi_change / oi_24h_ago * 100) if oi_24h_ago > 0 else 0 # 计算溢价率 premium_rate = 0 if funding_rate.get('index_price', 0) > 0: premium_rate = ((funding_rate['mark_price'] - funding_rate['index_price']) / funding_rate['index_price'] * 100) return { 'funding_rate': funding_rate, 'open_interest': open_interest, 'oi_change_24h': oi_change, 'oi_change_percent_24h': oi_change_percent, 'premium_rate': premium_rate, 'market_sentiment': funding_rate.get('sentiment', ''), 'sentiment_level': funding_rate.get('sentiment_level', '') } 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"• 标记价格: ${funding.get('mark_price', 0):,.2f}") lines.append(f"• 指数价格: ${funding.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} 张") lines.append(f"• 持仓金额: ${oi.get('open_interest_value', 0):,.0f}") # 持仓量变化 oi_change = market_data.get('oi_change_percent_24h', 0) if oi_change != 0: lines.append(f"• 24h变化: {oi_change:+.2f}%") if oi_change > 10: lines.append(f"• ⚠️ 持仓大幅增加,资金加速流入") elif oi_change < -10: lines.append(f"• ⚠️ 持仓大幅减少,资金加速流出") # 溢价率 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) # 全局实例 binance_service = BinanceService()