stock-ai-agent/backend/app/services/bitget_service.py
2026-02-28 22:59:12 +08:00

702 lines
23 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.

"""
Bitget UTA 数据服务 - 获取加密货币 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 BitgetService:
"""Bitget UTA 数据服务(使用 requests 直接调用 REST API"""
# K线周期映射 - 注意 Bitget 使用大写 H
INTERVALS = {
'1m': '1m',
'5m': '5m',
'15m': '15m',
'30m': '30m',
'1h': '1H', # Bitget 大写
}
# Bitget API 基础 URL
BASE_URL = "https://api.bitget.com"
TESTNET_URL = "https://api-testnet.bitget.com"
# 产品类型
CATEGORY_SPOT = 'SPOT'
CATEGORY_USDT_FUTURES = 'USDT-FUTURES'
CATEGORY_COIN_FUTURES = 'COIN-FUTURES'
CATEGORY_USDC_FUTURES = 'USDC-FUTURES'
def __init__(self, api_key: str = "", api_secret: str = "", use_testnet: bool = False):
"""
初始化 Bitget 服务
Args:
api_key: API 密钥(可选,公开数据不需要)
api_secret: API 密钥(可选)
use_testnet: 是否使用测试网
"""
self._api_key = api_key
self._api_secret = api_secret
self._base_url = self.TESTNET_URL if use_testnet else self.BASE_URL
self._session = requests.Session()
if api_key:
self._session.headers.update({
'ACCESS-KEY': api_key,
'ACCESS-SIGN': api_secret
})
logger.info(f"Bitget 服务初始化完成 ({'测试网' if use_testnet else '生产网'})")
def get_klines(self, symbol: str, interval: str, limit: int = 100,
category: str = None) -> pd.DataFrame:
"""
获取 K 线数据
Args:
symbol: 交易对,如 'BTCUSDT'
interval: K线周期'5m', '15m', '1h', '4h'
limit: 获取数量最大1000
category: 产品类型,默认 USDT-FUTURES
Returns:
DataFrame 包含 OHLCV 数据
"""
try:
if category is None:
category = self.CATEGORY_USDT_FUTURES
bitget_interval = self.INTERVALS.get(interval, interval)
url = f"{self._base_url}/api/v3/market/candles"
params = {
'category': category,
'symbol': symbol,
'interval': bitget_interval,
'limit': str(min(limit, 1000)) # Bitget 最大 1000
}
response = self._session.get(url, params=params, timeout=10)
response.raise_for_status()
result = response.json()
# Bitget 返回格式: {"code": "00000", "msg": "success", "requestTime": ..., "data": [...]}
if result.get('code') != '00000':
logger.error(f"Bitget API 错误: {result.get('msg')}")
return pd.DataFrame()
klines = result.get('data', [])
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,
category: str = None) -> Dict[str, pd.DataFrame]:
"""
获取多周期 K 线数据
Args:
symbol: 交易对
category: 产品类型,默认 USDT-FUTURES
Returns:
包含 1m, 5m, 15m, 30m, 1h 数据的字典
"""
# 不同周期使用不同的数据量,平衡分析深度和性能
# 1m: 200根 = 3.3小时(超短线精确入场)
# 5m: 200根 = 16.7小时(日内分析)
# 15m: 200根 = 2.1天(短线分析)
# 30m: 200根 = 4.2天(日内趋势)
# 1h: 300根 = 12.5天(日内主趋势)
limits = {
'1m': 200,
'5m': 200,
'15m': 200,
'30m': 200,
'1h': 300
}
data = {}
for interval in ['1m', '5m', '15m', '30m', '1h']:
df = self.get_klines(symbol, interval, limit=limits.get(interval, 100),
category=category)
if not df.empty:
df = self.calculate_indicators(df, interval)
data[interval] = df
logger.info(f"获取 {symbol} 多周期数据完成")
return data
def _parse_klines(self, klines: List) -> pd.DataFrame:
"""
解析 K 线数据为 DataFrame
Bitget 返回格式: [timestamp, open, high, low, close, base_volume, quote_volume]
"""
if not klines:
return pd.DataFrame()
df = pd.DataFrame(klines, columns=[
'open_time', 'open', 'high', 'low', 'close',
'base_volume', 'quote_volume'
])
# 转换数据类型
df['open_time'] = pd.to_datetime(df['open_time'].astype(int), unit='ms')
for col in ['open', 'high', 'low', 'close', 'base_volume', 'quote_volume']:
df[col] = df[col].astype(float)
# 重命名以匹配 Binance 格式
df = df.rename(columns={'base_volume': 'volume'})
# 只保留需要的列(与 Binance 保持一致)
df = df[['open_time', 'open', 'high', 'low', 'close', 'volume']]
# 按时间排序Bitget 返回的数据可能是倒序)
df = df.sort_values('open_time').reset_index(drop=True)
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 参数
ma_config = {
'1m': {'ma_short': 5, 'ma_mid': 10, 'ma_long': 20, 'ma_extra': 50},
'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},
'30m': {'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},
}
config = ma_config.get(interval, ma_config['1h'])
# EMA日内交易使用 EMA 反应更快)
df['ema5'] = self._calculate_ema(df['close'], config['ma_short'])
df['ema10'] = self._calculate_ema(df['close'], config['ma_mid'])
df['ema20'] = self._calculate_ema(df['close'], config['ma_long'])
df['ema50'] = self._calculate_ema(df['close'], config['ma_extra'])
# 兼容:保留 ma5/ma10/ma20/ma50 作为 EMA 的别名
df['ma5'] = df['ema5']
df['ma10'] = df['ema10']
df['ma20'] = df['ema20']
df['ma50'] = df['ema50']
# MACD 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 指标 - 使用 Wilder's Smoothing 方法"""
delta = data.diff()
gain = delta.where(delta > 0, 0)
loss = -delta.where(delta < 0, 0)
avg_gain = gain.ewm(alpha=1/period, adjust=False).mean()
avg_loss = loss.ewm(alpha=1/period, adjust=False).mean()
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, category: str = None) -> Optional[float]:
"""
获取当前价格
Args:
symbol: 交易对
category: 产品类型,默认 USDT-FUTURES
Returns:
当前价格
"""
try:
if category is None:
category = self.CATEGORY_USDT_FUTURES
url = f"{self._base_url}/api/v3/market/tickers"
params = {
'category': category,
'symbol': symbol
}
response = self._session.get(url, params=params, timeout=10)
response.raise_for_status()
result = response.json()
if result.get('code') != '00000':
logger.error(f"Bitget API 错误: {result.get('msg')}")
return None
data = result.get('data', [])
if not data:
return None
# 返回第一个也是唯一的ticker 的最新价格
return float(data[0].get('lastPrice', 0))
except Exception as e:
logger.error(f"获取 {symbol} 当前价格失败: {e}")
return None
def get_ticker(self, symbol: str, category: str = None) -> Optional[Dict[str, Any]]:
"""
获取完整的 Ticker 数据
Args:
symbol: 交易对
category: 产品类型,默认 USDT-FUTURES
Returns:
完整的 ticker 数据
"""
try:
if category is None:
category = self.CATEGORY_USDT_FUTURES
url = f"{self._base_url}/api/v3/market/tickers"
params = {
'category': category,
'symbol': symbol
}
response = self._session.get(url, params=params, timeout=10)
response.raise_for_status()
result = response.json()
if result.get('code') != '00000':
logger.error(f"Bitget API 错误: {result.get('msg')}")
return None
data = result.get('data', [])
if not data:
return None
return data[0]
except Exception as e:
logger.error(f"获取 {symbol} ticker 失败: {e}")
return None
def get_symbol_info(self, symbol: str, category: str = None) -> Optional[Dict[str, Any]]:
"""
获取交易对信息(包含精度配置)
Args:
symbol: 交易对
category: 产品类型,默认 USDT-FUTURES
Returns:
交易对信息,包含 pricePrecision 和 quantityPrecision
"""
try:
if category is None:
category = self.CATEGORY_USDT_FUTURES
url = f"{self._base_url}/api/v3/market/symbols"
params = {
'category': category,
'symbol': symbol
}
response = self._session.get(url, params=params, timeout=10)
response.raise_for_status()
result = response.json()
if result.get('code') != '00000':
logger.error(f"Bitget API 错误: {result.get('msg')}")
return None
data = result.get('data', [])
if not data:
return None
symbol_data = data[0]
# 返回精度信息
return {
'symbol': symbol_data.get('symbol'),
'status': symbol_data.get('status'),
'pricePrecision': int(symbol_data.get('pricePrecision', 2)),
'quantityPrecision': int(symbol_data.get('quantityPrecision', 2)),
'minTradeAmount': float(symbol_data.get('minTradeAmount', 0)),
'maxTradeAmount': float(symbol_data.get('maxTradeAmount', 0)),
'takerFeeRate': float(symbol_data.get('takerFeeRate', 0.001)),
'makerFeeRate': float(symbol_data.get('makerFeeRate', 0.001)),
}
except Exception as e:
logger.error(f"获取 {symbol} 交易对信息失败: {e}")
return None
# 缓存交易对精度信息
_symbol_precision_cache: Dict[str, Dict[str, int]] = {}
def get_precision(self, symbol: str, category: str = None) -> Dict[str, int]:
"""
获取交易对价格和数量精度(带缓存)
Args:
symbol: 交易对
category: 产品类型,默认 USDT-FUTURES
Returns:
{'pricePrecision': 2, 'quantityPrecision': 2}
"""
# 检查缓存
if symbol in self._symbol_precision_cache:
return self._symbol_precision_cache[symbol]
# 获取交易对信息
info = self.get_symbol_info(symbol, category)
if info:
precision = {
'pricePrecision': info['pricePrecision'],
'quantityPrecision': info['quantityPrecision']
}
# 缓存结果
self._symbol_precision_cache[symbol] = precision
return precision
# 默认精度
return {'pricePrecision': 2, 'quantityPrecision': 2}
def round_price(self, symbol: str, price: float, category: str = None) -> float:
"""
根据交易对精度四舍五入价格
Args:
symbol: 交易对
price: 原始价格
category: 产品类型
Returns:
四舍五入后的价格
"""
precision = self.get_precision(symbol, category)
return round(price, precision['pricePrecision'])
def round_quantity(self, symbol: str, quantity: float, category: str = None) -> float:
"""
根据交易对精度四舍五入数量
Args:
symbol: 交易对
quantity: 原始数量
category: 产品类型
Returns:
四舍五入后的数量
"""
precision = self.get_precision(symbol, category)
return round(quantity, precision['quantityPrecision'])
def get_funding_rate(self, symbol: str) -> Optional[Dict[str, Any]]:
"""
获取资金费率(包含标记价格和指数价格)
Args:
symbol: 交易对,如 'BTCUSDT'
Returns:
包含资金费率、标记价格、指数价格的字典
"""
try:
# 同时获取 ticker 数据(包含标记价格和指数价格)
ticker = self.get_ticker(symbol, self.CATEGORY_USDT_FUTURES)
if not ticker:
logger.error(f"获取 {symbol} ticker 数据失败")
return None
# 获取资金费率
url = f"{self._base_url}/api/v3/market/current-fund-rate"
params = {'symbol': symbol}
response = self._session.get(url, params=params, timeout=10)
response.raise_for_status()
result = response.json()
if result.get('code') != '00000':
logger.error(f"Bitget API 错误: {result.get('msg')}")
return None
data = result.get('data', [])
if not data:
return None
funding_data = data[0]
# 解析资金费率
funding_rate = float(funding_data.get('fundingRate', 0))
next_update = int(funding_data.get('nextUpdate', 0))
min_rate = float(funding_data.get('minFundingRate', 0))
max_rate = float(funding_data.get('maxFundingRate', 0))
# 从 ticker 获取标记价格和指数价格
mark_price = float(ticker.get('markPrice', 0))
index_price = float(ticker.get('indexPrice', 0))
# 判断市场情绪
if funding_rate > 0.01: # > 0.1%
sentiment = "极度贪婪"
sentiment_level = "extreme_greed"
elif funding_rate > 0.0005: # > 0.05%
sentiment = "贪婪"
sentiment_level = "greed"
elif funding_rate < -0.01: # < -0.1%
sentiment = "极度恐惧"
sentiment_level = "extreme_fear"
elif funding_rate < -0.0005: # < -0.05%
sentiment = "恐惧"
sentiment_level = "fear"
else:
sentiment = "中性"
sentiment_level = "neutral"
return {
'funding_rate': funding_rate,
'funding_rate_percent': funding_rate * 100,
'next_funding_time': next_update,
'min_funding_rate': min_rate,
'max_funding_rate': max_rate,
'mark_price': mark_price,
'index_price': index_price,
'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:
# 从 ticker 获取持仓量
ticker = self.get_ticker(symbol, self.CATEGORY_USDT_FUTURES)
if not ticker:
return None
open_interest = float(ticker.get('openInterest', 0))
return {
'open_interest': open_interest,
'timestamp': int(ticker.get('ts', 0))
}
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)
ticker = self.get_ticker(symbol, self.CATEGORY_USDT_FUTURES)
if not funding_rate or not open_interest or not ticker:
logger.warning(f"获取 {symbol} 合约数据不完整")
return None
# 计算溢价率
premium_rate = 0
index_price = float(ticker.get('indexPrice', 0))
mark_price = float(ticker.get('markPrice', 0))
if index_price > 0:
premium_rate = ((mark_price - index_price) / index_price * 100)
return {
'funding_rate': funding_rate,
'open_interest': open_interest,
'premium_rate': premium_rate,
'market_sentiment': funding_rate.get('sentiment', ''),
'sentiment_level': funding_rate.get('sentiment_level', ''),
'mark_price': mark_price,
'index_price': index_price
}
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"• 标记价格: ${market_data.get('mark_price', 0):,.2f}")
lines.append(f"• 指数价格: ${market_data.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}")
# 溢价率
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)
# 全局实例
bitget_service = BitgetService()