feat: websocket kline fetch and enforce proxy routing

This commit is contained in:
crypto_predictor committed 2026-03-07 12:12:49 +08:00
1 parent 7af21bed6a
commit 77d084341c
5 files changed
+280 -45

No files matched your search

+134 -32
View File
@@ -1,15 +1,19 @@
"""从Binance获取加密货币OHLCV数据"""
import os
import json
import time
from datetime import datetime, timedelta
from pathlib import Path
from typing import Optional, Literal
from typing import Optional, Literal, Any
import logging
from urllib.parse import urlparse
import pandas as pd
from binance.client import Client
import pytz
from dotenv import load_dotenv
from websocket import create_connection
from data.database import DatabaseManager
from utils.send_msg import lark_notifier
@@ -18,6 +22,19 @@ from utils.send_msg import lark_notifier
load_dotenv()
logger = logging.getLogger("polymarket_trader")
def _apply_global_proxy(proxy_url: str) -> None:
"""统一设置代理环境变量,确保第三方网络库也走代理。"""
os.environ['HTTP_PROXY'] = proxy_url
os.environ['HTTPS_PROXY'] = proxy_url
os.environ['ALL_PROXY'] = proxy_url
os.environ['http_proxy'] = proxy_url
os.environ['https_proxy'] = proxy_url
os.environ['all_proxy'] = proxy_url
class BinanceDataFetcher:
"""从Binance公开API获取和缓存OHLCV数据"""
@@ -26,6 +43,13 @@ class BinanceDataFetcher:
'15m': Client.KLINE_INTERVAL_15MINUTE,
}
INTERVAL_MS = {
'1h': 60 * 60 * 1000,
'15m': 15 * 60 * 1000,
}
WS_API_ENDPOINT = "wss://ws-api.binance.com/ws-api/v3"
def __init__(self, cache_dir: str = "./data/cache"):
"""初始化Binance客户端和数据库管理器
@@ -33,33 +57,24 @@ class BinanceDataFetcher:
cache_dir: 缓存/数据库目录
"""
# 配置代理
proxies = None
use_proxy = os.getenv('USE_PROXY', 'false').lower() == 'true'
if use_proxy:
socks5_proxy = os.getenv('SOCKS5_PROXY', 'socks5://127.0.0.1:7890')
proxies = {
'http': socks5_proxy,
'https': socks5_proxy
}
print(f"✓ 使用SOCKS5代理: {socks5_proxy}")
socks5_proxy = os.getenv('SOCKS5_PROXY', 'socks5://127.0.0.1:7890')
_apply_global_proxy(socks5_proxy)
proxies = {
'http': socks5_proxy,
'https': socks5_proxy
}
logger.info(f"强制使用SOCKS5代理: {socks5_proxy}")
try:
# 初始化Binance客户端(带代理)
if proxies:
self.client = Client(requests_params={'proxies': proxies})
else:
self.client = Client()
# 初始化Binance客户端(强制走代理)
self.client = Client(requests_params={'proxies': proxies})
# 测试连接
self.client.ping()
print("✓ 已成功连接到Binance API")
logger.info("已成功连接到Binance API")
except Exception as e:
print(f"⚠ Binance API连接失败: {e}")
print("提示:")
print(" 1. 检查代理设置 (.env文件)")
print(" 2. 确保代理服务正在运行")
print(" 3. 或者使用已缓存的数据")
logger.warning(f"Binance API连接失败: {e}")
logger.warning("提示: 1) 检查代理设置 2) 确保代理服务运行 3) 或者使用缓存数据")
self.client = None
# 初始化数据库管理器
@@ -112,13 +127,13 @@ class BinanceDataFetcher:
# 简单判断覆盖范围 (容忍少量缺失)
if min_ts <= start_dt + timedelta(days=2) and max_ts >= end_dt - timedelta(days=2):
print(f"从数据库加载数据:{symbol} {timeframe} ({len(df)} rows)")
logger.info(f"从数据库加载数据: {symbol} {timeframe} ({len(df)} rows)")
return df
# 2. 从API获取数据
if self.client is None:
if not df.empty:
print(f"⚠ API不可用,返回数据库现有数据 ({len(df)} rows)")
logger.warning(f"API不可用,返回数据库现有数据 ({len(df)} rows)")
return df
raise ConnectionError(
@@ -128,32 +143,35 @@ class BinanceDataFetcher:
# Binance使用USDT交易对
pair = f"{symbol}USDT"
interval = self.INTERVALS[timeframe]
interval_ms = self.INTERVAL_MS[timeframe]
print(f"获取 {symbol} {timeframe} 数据,时间范围:{start_dt.date()} 至 {end_dt.date()}...")
logger.info(f"获取 {symbol} {timeframe} 数据,时间范围: {start_dt.date()} 至 {end_dt.date()}")
klines = []
for attempt in range(max_retries):
try:
klines = self.client.get_historical_klines(
# 使用 Binance WebSocket API 的 klines 方法获取历史K线
klines = self._get_historical_klines_via_ws(
symbol=pair,
interval=interval,
start_str=str(int(start_dt.timestamp() * 1000)),
end_str=str(int(end_dt.timestamp() * 1000))
start_ts_ms=int(start_dt.timestamp() * 1000),
end_ts_ms=int(end_dt.timestamp() * 1000),
interval_ms=interval_ms
)
# 如果成功获取,跳出循环
break
except Exception as e:
# 如果是最后一次尝试,且依然失败
if attempt == max_retries - 1:
print(f"❌ 第 {attempt + 1} 次尝试失败,已达到最大重试次数。错误: {e}")
logger.error(f"第 {attempt + 1} 次尝试失败,已达到最大重试次数。错误: {e}")
if not df.empty:
print(f"⚠ API请求最终失败,返回数据库现有数据")
logger.warning("WebSocket请求最终失败,返回数据库现有数据")
return df
raise e # 如果没有缓存数据,抛出异常
# 计算等待时间 (指数退避: 2s, 4s, 8s...)
wait_time = 2 * (2 ** attempt)
print(f"⚠ 请求失败 (尝试 {attempt + 1}/{max_retries}),{wait_time}秒后重试... 错误: {e}")
logger.warning(f"请求失败 (尝试 {attempt + 1}/{max_retries}),{wait_time}秒后重试。错误: {e}")
time.sleep(wait_time)
if not klines:
@@ -179,13 +197,94 @@ class BinanceDataFetcher:
# 按时间排序
new_df = new_df.sort_values('timestamp').reset_index(drop=True)
print(f"已获取 {len(new_df)} 行数据")
logger.info(f"已获取 {len(new_df)} 行数据")
# 保存到数据库
self.db_manager.save_ohlcv(new_df, symbol, timeframe)
return new_df
def _get_historical_klines_via_ws(
self,
symbol: str,
interval: str,
start_ts_ms: int,
end_ts_ms: int,
interval_ms: int,
limit: int = 1000
):
"""通过 Binance WebSocket API 分页拉取历史K线。"""
if end_ts_ms <= start_ts_ms:
return []
all_klines = []
request_id = 1
current_start = start_ts_ms
# WebSocket API 单次最多返回 1000 条,这里按窗口分页拉取。
while current_start < end_ts_ms:
max_window_end = current_start + (limit * interval_ms)
current_end = min(end_ts_ms, max_window_end)
payload = {
"id": request_id,
"method": "klines",
"params": {
"symbol": symbol,
"interval": interval,
"startTime": current_start,
"endTime": current_end,
"limit": limit
}
}
response_data = self._ws_request(payload)
batch = response_data.get("result", [])
if not batch:
break
all_klines.extend(batch)
last_open_time = int(batch[-1][0])
next_start = last_open_time + interval_ms
if next_start <= current_start:
break
current_start = next_start
request_id += 1
return all_klines
def _ws_request(self, payload: dict) -> dict:
"""发送单次 WebSocket API 请求并返回 JSON 响应。"""
ws_kwargs: dict[str, Any] = {
"timeout": 30
}
socks5_proxy = os.getenv('SOCKS5_PROXY', 'socks5://127.0.0.1:7890')
parsed = urlparse(socks5_proxy)
if parsed.hostname and parsed.port:
scheme = (parsed.scheme or 'socks5').lower()
if scheme not in {'socks4', 'socks5', 'http'}:
scheme = 'socks5'
if scheme == 'socks5h':
scheme = 'socks5'
ws_kwargs.update({
"http_proxy_host": parsed.hostname,
"http_proxy_port": parsed.port,
"proxy_type": scheme
})
ws = create_connection(self.WS_API_ENDPOINT, **ws_kwargs)
try:
ws.send(json.dumps(payload))
raw = ws.recv()
data = json.loads(raw)
status = data.get("status", 200)
if status != 200:
raise RuntimeError(f"Binance WS API返回异常状态: {data}")
return data
finally:
ws.close()
def get_latest_price(self, symbol: Literal['BTC', 'ETH', 'SOL']) -> float:
"""获取符号的当前价格
@@ -195,6 +294,9 @@ class BinanceDataFetcher:
Returns:
当前USDT价格
"""
if self.client is None:
raise ConnectionError("Binance客户端不可用,无法获取最新价格")
pair = f"{symbol}USDT"
ticker = self.client.get_symbol_ticker(symbol=pair)
return float(ticker['price'])