""" Binance 数据服务 - 获取加密货币 K 线数据和技术指标 """ import pandas as pd import numpy as np from typing import Dict, List, Optional, Any from binance.client import Client from binance.enums import ( KLINE_INTERVAL_5MINUTE, KLINE_INTERVAL_15MINUTE, KLINE_INTERVAL_1HOUR, KLINE_INTERVAL_4HOUR ) from app.utils.logger import logger class BinanceService: """Binance 数据服务""" # K线周期映射 INTERVALS = { '5m': KLINE_INTERVAL_5MINUTE, '15m': KLINE_INTERVAL_15MINUTE, '1h': KLINE_INTERVAL_1HOUR, '4h': KLINE_INTERVAL_4HOUR } def __init__(self, api_key: str = "", api_secret: str = ""): """ 初始化 Binance 客户端 Args: api_key: API 密钥(可选,公开数据不需要) api_secret: API 密钥(可选) """ self.client = Client(api_key=api_key, api_secret=api_secret) 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) klines = self.client.get_klines( symbol=symbol, interval=binance_interval, limit=limit ) 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 数据的字典 """ data = {} for interval in ['5m', '15m', '1h', '4h']: df = self.get_klines(symbol, interval, limit=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']) 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: ticker = self.client.get_symbol_ticker(symbol=symbol) 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: stats = self.client.get_ticker(symbol=symbol) 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 # 全局实例 binance_service = BinanceService()