531 lines
18 KiB
Python
531 lines
18 KiB
Python
"""
|
||
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, interval) # 传递 interval 参数
|
||
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, interval: str = '1h') -> pd.DataFrame:
|
||
"""
|
||
计算技术指标
|
||
|
||
Args:
|
||
df: K线数据 DataFrame
|
||
interval: K线周期,用于调整 MA 参数
|
||
|
||
Returns:
|
||
添加了技术指标的 DataFrame
|
||
"""
|
||
if df.empty:
|
||
return df
|
||
|
||
# 根据周期调整 MA 参数
|
||
# 短周期(5m, 15m)使用较短的 MA,长周期使用较长的 MA
|
||
ma_config = {
|
||
'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},
|
||
'1h': {'ma_short': 5, 'ma_mid': 10, 'ma_long': 20, 'ma_extra': 50},
|
||
'4h': {'ma_short': 5, 'ma_mid': 10, 'ma_long': 20, 'ma_extra': 50},
|
||
}
|
||
|
||
config = ma_config.get(interval, ma_config['1h'])
|
||
|
||
# 移动平均线(统一命名,便于 LLM 分析)
|
||
df['ma5'] = self._calculate_ma(df['close'], config['ma_short'])
|
||
df['ma10'] = self._calculate_ma(df['close'], config['ma_mid'])
|
||
df['ma20'] = self._calculate_ma(df['close'], config['ma_long'])
|
||
df['ma50'] = self._calculate_ma(df['close'], config['ma_extra'])
|
||
|
||
# EMA
|
||
df['ema12'] = self._calculate_ema(df['close'], 12)
|
||
df['ema26'] = self._calculate_ema(df['close'], 26)
|
||
|
||
# RSI(使用 Wilder's Smoothing)
|
||
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 方法
|
||
这是标准 RSI 计算方法,比简单平均更准确
|
||
"""
|
||
delta = data.diff()
|
||
|
||
# 分离涨跌
|
||
gain = delta.where(delta > 0, 0)
|
||
loss = -delta.where(delta < 0, 0)
|
||
|
||
# 使用 Wilder's Smoothing (EMA) 而不是简单平均
|
||
# alpha = 1/period
|
||
avg_gain = gain.ewm(alpha=1/period, adjust=False).mean()
|
||
avg_loss = loss.ewm(alpha=1/period, adjust=False).mean()
|
||
|
||
# 计算 RS 和 RSI
|
||
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) -> 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()
|