diff --git a/datasource/flask_api_source.py b/datasource/flask_api_source.py index 2524ab2..81902b9 100644 --- a/datasource/flask_api_source.py +++ b/datasource/flask_api_source.py @@ -8,8 +8,7 @@ Flask API 数据源 import os import json import time -import urllib3 -import urllib.parse +import requests import pandas as pd from typing import Optional, Dict, List from datetime import datetime @@ -21,23 +20,17 @@ from .models import OHLCVResponse, validate_ohlcv_response load_dotenv() # ============================================================ -# HTTP client (urllib3 替代 requests,修复 SSL EOF 问题) +# HTTP client (requests + trust_env=False,绕过系统代理避免 SSL EOF) # ============================================================ -_http_pool = urllib3.PoolManager( - maxsize=16, # 支持并行连接 - timeout=urllib3.Timeout(connect=10, read=120) -) +# Clash 等代理在处理 TLS 1.3 + 后量子密钥交换时会触发 SSL EOF 错误 +# trust_env=False 让 requests 忽略环境变量中的代理配置,直连目标服务器 +_session = requests.Session() +_session.trust_env = False -def _http_get(url: str, params: dict = None, timeout: int = 120) -> urllib3.HTTPResponse: - """使用 urllib3 发起 GET 请求(替代 requests.get,修复 OpenSSL 3.5 + Caddy 的 SSL EOF 问题)""" - if params: - url = url + '?' + urllib.parse.urlencode(params) - return _http_pool.request('GET', url, timeout=urllib3.Timeout(connect=10, read=timeout)) - -def _parse_json(resp: urllib3.HTTPResponse) -> dict: - """解析 JSON 响应""" - return json.loads(resp.data.decode('utf-8')) +def _http_get(url: str, params: dict = None, timeout: int = 120) -> requests.Response: + """使用 requests 发起 GET 请求(trust_env=False 绕过系统代理)""" + return _session.get(url, params=params, timeout=timeout) class FlaskAPIDataSource: @@ -133,15 +126,15 @@ class FlaskAPIDataSource: try: response = _http_get(url, params=params, timeout=self.timeout) - if response.status != 200: + if response.status_code != 200: if attempt < self.retries - 1: time.sleep(1 + attempt) continue - print(f"✗ API请求失败: {response.status} - {response.data.decode('utf-8', errors='replace')[:100]}") + print(f"✗ API请求失败: {response.status_code} - {response.text[:100]}") return None # 解析 JSON - data = _parse_json(response) + data = response.json() # 检查错误 if 'error' in data: @@ -210,7 +203,7 @@ class FlaskAPIDataSource: print(f"✓ {code}: {actual_count} 条数据 ({actual_start} ~ {actual_end})") return df - except urllib3.exceptions.TimeoutError: + except requests.exceptions.Timeout: if attempt < self.retries - 1: print(f"⚠ {code}: 请求超时,重试 {attempt + 2}/{self.retries}") time.sleep(1 + attempt) @@ -218,7 +211,7 @@ class FlaskAPIDataSource: print(f"✗ {code}: 请求超时") return None - except (urllib3.exceptions.SSLError, urllib3.exceptions.MaxRetryError, urllib3.exceptions.ProtocolError) as e: + except (requests.exceptions.SSLError, requests.exceptions.ConnectionError) as e: if attempt < self.retries - 1: print(f"⚠ {code}: {type(e).__name__},重试 {attempt + 2}/{self.retries}") time.sleep(1 + attempt) @@ -226,7 +219,7 @@ class FlaskAPIDataSource: print(f"✗ {code}: {type(e).__name__} after {self.retries} retries") return None - except urllib3.exceptions.HTTPError as e: + except requests.exceptions.RequestException as e: if attempt < self.retries - 1: time.sleep(1 + attempt) continue @@ -303,10 +296,10 @@ class FlaskAPIDataSource: try: response = _http_get(url, params=params, timeout=self.timeout) - if response.status != 200: + if response.status_code != 200: return None - data = _parse_json(response) + data = response.json() if 'error' in data: return None @@ -378,8 +371,8 @@ class FlaskAPIDataSource: try: response = _http_get(url, params=params, timeout=self.timeout) - if response.status == 200: - data = _parse_json(response) + if response.status_code == 200: + data = response.json() return { 'status': 'healthy', 'ssh_configured': True, @@ -396,10 +389,10 @@ class FlaskAPIDataSource: try: response = _http_get(url, timeout=10) - if response.status == 200: - return _parse_json(response) + if response.status_code == 200: + return response.json() else: - return {"error": f"HTTP {response.status}"} + return {"error": f"HTTP {response.status_code}"} except Exception as e: return {"error": str(e)} @@ -442,15 +435,15 @@ class FlaskAPIDataSource: try: response = _http_get(url, params=params, timeout=self.timeout) - if response.status != 200: + if response.status_code != 200: if attempt < self.retries - 1: - print(f"⚠ 交易日历请求失败 (HTTP {response.status}),重试 {attempt + 2}/{self.retries}") + print(f"⚠ 交易日历请求失败 (HTTP {response.status_code}),重试 {attempt + 2}/{self.retries}") time.sleep(1 + attempt) continue - print(f"✗ 交易日历请求失败: HTTP {response.status} - {response.data.decode('utf-8', errors='replace')[:100]}") + print(f"✗ 交易日历请求失败: HTTP {response.status_code} - {response.text[:100]}") return None - data = _parse_json(response) + data = response.json() # 检查错误 if 'error' in data: @@ -471,7 +464,7 @@ class FlaskAPIDataSource: print(f"✓ {market} ({exchange}): {count} 个交易日 ({start_date} ~ {end_date})") return dates - except urllib3.exceptions.TimeoutError: + except requests.exceptions.Timeout: if attempt < self.retries - 1: print(f"⚠ 交易日历请求超时,重试 {attempt + 2}/{self.retries}") time.sleep(1 + attempt) @@ -479,7 +472,7 @@ class FlaskAPIDataSource: print(f"✗ 交易日历请求超时") return None - except (urllib3.exceptions.SSLError, urllib3.exceptions.MaxRetryError, urllib3.exceptions.ProtocolError) as e: + except (requests.exceptions.SSLError, requests.exceptions.ConnectionError) as e: if attempt < self.retries - 1: print(f"⚠ 交易日历: {type(e).__name__},重试 {attempt + 2}/{self.retries}") time.sleep(1 + attempt) @@ -487,7 +480,7 @@ class FlaskAPIDataSource: print(f"✗ 交易日历: {type(e).__name__} after {self.retries} retries") return None - except urllib3.exceptions.HTTPError as e: + except requests.exceptions.RequestException as e: if attempt < self.retries - 1: time.sleep(1 + attempt) continue @@ -506,7 +499,7 @@ class FlaskAPIDataSource: try: response = _http_get(url, timeout=10) - return _parse_json(response) + return response.json() except Exception as e: return {"error": str(e)} diff --git a/rotation/simple_rotation.py b/rotation/simple_rotation.py index a0ceff3..b5b21c2 100644 --- a/rotation/simple_rotation.py +++ b/rotation/simple_rotation.py @@ -14,8 +14,7 @@ import sys import math import json import time -import urllib3 -import urllib.parse +import requests import numpy as np import pandas as pd from pathlib import Path @@ -28,31 +27,17 @@ sys.path.insert(0, str(PROJECT_ROOT)) from rotation.config_loader import load_rotation_config, RotationStrategyConfig # ============================================================ -# HTTP client (urllib3 替代 requests,修复 SSL EOF 问题) +# HTTP client (requests + trust_env=False,绕过系统代理避免 SSL EOF) # ============================================================ -_http_pool = urllib3.PoolManager( - maxsize=16, # 支持并行连接 - timeout=urllib3.Timeout(connect=10, read=120) -) +# Clash 等代理在处理 TLS 1.3 + 后量子密钥交换时会触发 SSL EOF 错误 +# trust_env=False 让 requests 忽略环境变量中的代理配置,直连目标服务器 +_session = requests.Session() +_session.trust_env = False -class _HttpResponse: - """urllib3 响应包装,提供 requests 兼容接口""" - def __init__(self, resp): - self.status_code = resp.status - self._data = resp.data - self._json = None - def json(self): - if self._json is None: - self._json = json.loads(self._data) - return self._json - -def _http_get(url: str, params: dict = None, timeout: int = 120) -> _HttpResponse: - """使用 urllib3 发起 GET 请求(替代 requests.get,修复 OpenSSL 3.5 + Caddy 的 SSL EOF 问题)""" - if params: - url = url + '?' + urllib.parse.urlencode(params) - resp = _http_pool.request('GET', url, timeout=urllib3.Timeout(connect=10, read=timeout)) - return _HttpResponse(resp) +def _http_get(url: str, params: dict = None, timeout: int = 120) -> requests.Response: + """使用 requests 发起 GET 请求(trust_env=False 绕过系统代理)""" + return _session.get(url, params=params, timeout=timeout) # ============================================================ @@ -177,7 +162,7 @@ class DataCache: self._save_premium_cache(code, df.attrs['premium_series']) print(f" + {code}: {len(df)} rows ({adj})") return df - except (urllib3.exceptions.TimeoutError, urllib3.exceptions.SSLError, urllib3.exceptions.MaxRetryError, urllib3.exceptions.ProtocolError) as e: + except (requests.exceptions.Timeout, requests.exceptions.SSLError, requests.exceptions.ConnectionError) as e: # 网络相关错误(超时、SSL、连接断开)都进行重试 if attempt < 2: time.sleep(1 + attempt) # 递增延迟: 1s, 2s @@ -255,7 +240,7 @@ class DataCache: self._save_premium_cache(code, self.premium_data[code]) print(f" + premium {code}: +{len(new_data)} days (total {len(self.premium_data[code])})") return - except (urllib3.exceptions.TimeoutError, urllib3.exceptions.SSLError, urllib3.exceptions.MaxRetryError, urllib3.exceptions.ProtocolError): + except (requests.exceptions.Timeout, requests.exceptions.SSLError, requests.exceptions.ConnectionError): if attempt < 2: time.sleep(1 + attempt) continue @@ -285,7 +270,7 @@ class DataCache: result = pd.DatetimeIndex(dates) print(f" + {market}: {len(result)} trading days ({start_date} ~ {end_date})") return result - except (urllib3.exceptions.TimeoutError, urllib3.exceptions.SSLError, urllib3.exceptions.MaxRetryError, urllib3.exceptions.ProtocolError) as e: + except (requests.exceptions.Timeout, requests.exceptions.SSLError, requests.exceptions.ConnectionError) as e: if attempt < 2: time.sleep(1 + attempt) continue