247 lines
7.8 KiB
Python
247 lines
7.8 KiB
Python
"""
|
||
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()
|