"""从Binance获取加密货币OHLCV数据""" import os import time from datetime import datetime, timedelta from pathlib import Path from typing import Optional, Literal import pandas as pd from binance.client import Client import pytz from dotenv import load_dotenv from data.database import DatabaseManager from utils.network_proxy import apply_global_proxy_from_env, get_proxy_url from utils.project_logger import get_project_logger from utils.send_msg import lark_notifier # 加载环境变量 load_dotenv() apply_global_proxy_from_env() logger = get_project_logger("data.fetcher") class BinanceDataFetcher: """从Binance公开API获取和缓存OHLCV数据""" INTERVALS = { '1h': Client.KLINE_INTERVAL_1HOUR, '15m': Client.KLINE_INTERVAL_15MINUTE, } def __init__(self, cache_dir: str = "./data/cache"): """初始化Binance客户端和数据库管理器 Args: cache_dir: 缓存/数据库目录 """ # 统一读取代理配置,兼容全局代理环境变量。 proxies = None proxy_url = get_proxy_url() if proxy_url: proxies = { 'http': proxy_url, 'https': proxy_url } logger.info("使用代理: %s", proxy_url) try: # 初始化Binance客户端(带代理) if proxies: self.client = Client(requests_params={'proxies': proxies}) else: self.client = Client() # 测试连接 self.client.ping() logger.info("已成功连接到Binance API") except Exception as e: logger.warning("Binance API连接失败: %s", e) logger.warning("提示: 检查代理设置、代理服务状态,或使用缓存数据") self.client = None # 初始化数据库管理器 self.db_manager = DatabaseManager(os.path.join(cache_dir, "market_data.db")) def fetch_ohlcv( self, symbol: Literal['BTC', 'ETH', 'SOL'], timeframe: Literal['1h', '15m'], start_date: Optional[str] = None, end_date: Optional[str] = None, use_cache: bool = True, max_retries: int = 3 # [新增] 最大重试次数参数 ) -> pd.DataFrame: """获取加密货币对的OHLCV数据 Args: symbol: 加密货币符号 (BTC, ETH, SOL) timeframe: 时间框架间隔 (1h 或 15m) start_date: 开始日期,格式:'YYYY-MM-DD' (默认: 1年前) end_date: 结束日期,格式:'YYYY-MM-DD' (默认: 现在) use_cache: 是否使用缓存数据 Returns: 包含以下列的DataFrame: timestamp, open, high, low, close, volume """ # 设置日期范围 if end_date is None: end_dt = datetime.now(pytz.UTC) else: end_dt = datetime.strptime(end_date, '%Y-%m-%d').replace(tzinfo=pytz.UTC) if start_date is None: start_dt = end_dt - timedelta(days=365) # 1年前 else: start_dt = datetime.strptime(start_date, '%Y-%m-%d').replace(tzinfo=pytz.UTC) start_str = start_dt.strftime('%Y-%m-%d') end_str = end_dt.strftime('%Y-%m-%d') # 1. 尝试从数据库获取数据 df = pd.DataFrame() if use_cache: df = self.db_manager.get_ohlcv(symbol, timeframe, start_str, end_str) # 检查数据是否足够 if not df.empty: min_ts = df['timestamp'].min().replace(tzinfo=pytz.UTC) max_ts = df['timestamp'].max().replace(tzinfo=pytz.UTC) # 简单判断覆盖范围 (容忍少量缺失) if min_ts <= start_dt + timedelta(days=2) and max_ts >= end_dt - timedelta(days=2): logger.info("从数据库加载数据: %s %s (%s rows)", symbol, timeframe, len(df)) return df # 2. 从API获取数据 if self.client is None: if not df.empty: logger.warning("API不可用,返回数据库现有数据 (%s rows)", len(df)) return df raise ConnectionError( "Binance客户端不可用。请使用VPN或确保数据库有数据。" ) # Binance使用USDT交易对 pair = f"{symbol}USDT" interval = self.INTERVALS[timeframe] logger.info("获取 %s %s 数据,时间范围: %s 至 %s", symbol, timeframe, start_dt.date(), end_dt.date()) klines = [] for attempt in range(max_retries): try: klines = self.client.get_historical_klines( symbol=pair, interval=interval, start_str=str(int(start_dt.timestamp() * 1000)), end_str=str(int(end_dt.timestamp() * 1000)) ) # 如果成功获取,跳出循环 break except Exception as e: # 如果是最后一次尝试,且依然失败 if attempt == max_retries - 1: logger.error("第 %s 次尝试失败,已达到最大重试次数。错误: %s", attempt + 1, e) if not df.empty: logger.warning("API请求最终失败,返回数据库现有数据") return df raise e # 如果没有缓存数据,抛出异常 # 计算等待时间 (指数退避: 2s, 4s, 8s...) wait_time = 2 * (2 ** attempt) logger.warning("请求失败(尝试 %s/%s),%s 秒后重试,错误: %s", attempt + 1, max_retries, wait_time, e) time.sleep(wait_time) if not klines: lark_notifier.send_alert(f"数据获取失败: {symbol} {timeframe}", "连续3次请求或解析发生异常,已放弃重试") if not df.empty: return df raise ValueError(f"未获取到 {symbol} {timeframe} 的数据") # 转换为DataFrame new_df = pd.DataFrame(klines, columns=[ 'timestamp', 'open', 'high', 'low', 'close', 'volume', 'close_time', 'quote_volume', 'trades', 'taker_buy_base', 'taker_buy_quote', 'ignore' ]) # 选择并转换列 new_df = new_df[['timestamp', 'open', 'high', 'low', 'close', 'volume']].copy() new_df['timestamp'] = pd.to_datetime(new_df['timestamp'], unit='ms') for col in ['open', 'high', 'low', 'close', 'volume']: new_df[col] = new_df[col].astype(float) # 按时间排序 new_df = new_df.sort_values('timestamp').reset_index(drop=True) logger.info("已获取 %s 行数据", len(new_df)) # 保存到数据库 self.db_manager.save_ohlcv(new_df, symbol, timeframe) return new_df def get_latest_price(self, symbol: Literal['BTC', 'ETH', 'SOL']) -> float: """获取符号的当前价格 Args: symbol: 加密货币符号 Returns: 当前USDT价格 """ pair = f"{symbol}USDT" ticker = self.client.get_symbol_ticker(symbol=pair) return float(ticker['price']) if __name__ == "__main__": # 测试数据获取器 fetcher = BinanceDataFetcher() # 获取BTC 1小时数据(1年) df = fetcher.fetch_ohlcv('BTC', '1h') print("\nBTC 1小时数据:") print(df.head()) print(f"\n形状:{df.shape}") print(f"日期范围:{df['timestamp'].min()} 至 {df['timestamp'].max()}") # 获取当前价格 try: current_price = fetcher.get_latest_price('BTC') print(f"\n当前BTC价格:${current_price:,.2f}") except: print("\n无法获取当前价格")