stock-ai-agent/backend/app/services/binance_service.py
2026-02-20 23:56:02 +08:00

531 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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()