feat(datasource): 实现加密货币数据获取功能

- 新增 ccxt_source.py: CCXT + OKX 加密货币数据源
- 新增 socks2http.py: SOCKS5 转 HTTP 代理转换器
- 修改 universal_fetcher.py: 添加 _fetch_crypto 方法,支持 timeframe 参数
- 修改 flask_server.py: API 支持 timeframe 参数,加密货币不缓存

支持的 timeframe: 1d, 1h, 4h, 15m, 1m
测试验证: BTC 数据获取成功
This commit is contained in:
2026-05-13 23:30:32 +08:00
parent 105af19690
commit 416f708d53
4 changed files with 513 additions and 13 deletions

168
datasource/socks2http.py Normal file
View File

@@ -0,0 +1,168 @@
#!/usr/bin/env python3
"""
SOCKS5 转 HTTP 代理转发工具
将 SSH 隧道的 SOCKS5 代理 (1080) 转为 HTTP 代理 (8080)
供 CCXT 等只支持 HTTP 代理的库使用
"""
import socket
import threading
import select
import sys
from urllib.parse import urlparse
class Socks2Http:
def __init__(self, socks_host='127.0.0.1', socks_port=1080, http_port=8080):
self.socks_host = socks_host
self.socks_port = socks_port
self.http_port = http_port
self.server = None
self.running = False
def start(self):
"""启动 HTTP 代理服务器"""
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server.bind(('127.0.0.1', self.http_port))
self.server.listen(5)
self.running = True
print(f"HTTP 代理已启动: http://127.0.0.1:{self.http_port}")
print(f"转发到 SOCKS5: {self.socks_host}:{self.socks_port}")
while self.running:
try:
client, addr = self.server.accept()
thread = threading.Thread(target=self._handle_client, args=(client,))
thread.daemon = True
thread.start()
except Exception as e:
if self.running:
print(f"接受连接错误: {e}")
def stop(self):
"""停止代理服务器"""
self.running = False
if self.server:
self.server.close()
print("HTTP 代理已停止")
def _handle_client(self, client):
"""处理客户端连接"""
try:
# 读取 HTTP 请求
request = client.recv(4096)
if not request:
client.close()
return
# 解析 CONNECT 请求
first_line = request.split(b'\r\n')[0].decode('utf-8', errors='ignore')
if first_line.startswith('CONNECT'):
# HTTPS 代理
parts = first_line.split()
if len(parts) >= 2:
target = parts[1]
host, port = target.rsplit(':', 1)
port = int(port)
# 连接到 SOCKS5 代理
remote = self._connect_via_socks5(host, port)
if remote:
client.send(b'HTTP/1.1 200 Connection established\r\n\r\n')
self._relay(client, remote)
else:
client.send(b'HTTP/1.1 502 Bad Gateway\r\n\r\n')
else:
# HTTP 代理
lines = first_line.split()
if len(lines) >= 2:
url = lines[1]
parsed = urlparse(url)
host = parsed.hostname
port = parsed.port or 80
# 连接到 SOCKS5 代理
remote = self._connect_via_socks5(host, port)
if remote:
# 修改请求,去掉完整 URL
new_request = request.replace(
f'{lines[0]} {url} '.encode(),
f'{lines[0]} {parsed.path or "/"}{"?" + parsed.query if parsed.query else ""} '.encode()
)
remote.send(new_request)
self._relay(client, remote)
except Exception as e:
print(f"处理客户端错误: {e}")
finally:
client.close()
def _connect_via_socks5(self, host, port):
"""通过 SOCKS5 代理连接目标服务器"""
try:
# 连接到 SOCKS5 代理
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(30)
sock.connect((self.socks_host, self.socks_port))
# SOCKS5 握手
# 1. 发送认证方法
sock.send(b'\x05\x01\x00') # VER=5, NMETHODS=1, METHOD=0 (无认证)
resp = sock.recv(2)
if resp[0] != 0x05 or resp[1] != 0x00:
sock.close()
return None
# 2. 发送连接请求
req = b'\x05\x01\x00\x03' # VER=5, CMD=CONNECT, RSV=0, ATYP=DOMAIN
req += bytes([len(host)]) + host.encode()
req += bytes([(port >> 8) & 0xFF, port & 0xFF])
sock.send(req)
# 3. 读取响应
resp = sock.recv(10)
if len(resp) < 4 or resp[1] != 0x00:
sock.close()
return None
return sock
except Exception as e:
print(f"SOCKS5 连接错误: {e}")
return None
def _relay(self, client, remote):
"""双向转发数据"""
try:
while True:
readable, _, _ = select.select([client, remote], [], [], 60)
if not readable:
break
if client in readable:
data = client.recv(4096)
if not data:
break
remote.send(data)
if remote in readable:
data = remote.recv(4096)
if not data:
break
client.send(data)
except:
pass
finally:
client.close()
remote.close()
if __name__ == '__main__':
proxy = Socks2Http(socks_port=1080, http_port=8080)
try:
proxy.start()
except KeyboardInterrupt:
proxy.stop()