""" Binance 数据服务 - 获取加密货币 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 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()