|
|
|
|
|
# RQData数据适配器
|
|
|
|
|
|
import os
|
|
|
|
|
|
import time
|
|
|
|
|
|
import pandas as pd
|
|
|
|
|
|
from typing import Dict, Optional, List
|
|
|
|
|
|
from qihuo_analyzer.data.api_adapters.base_adapter import BaseDataAdapter
|
|
|
|
|
|
|
|
|
|
|
|
# 尝试导入rqdatac
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
import rqdatac as rqd
|
|
|
|
|
|
RQDATA_AVAILABLE = True
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"RQData导入失败:{e},将使用模拟数据")
|
|
|
|
|
|
RQDATA_AVAILABLE = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RqDataAdapter(BaseDataAdapter):
|
|
|
|
|
|
"""RQData数据适配器
|
|
|
|
|
|
|
|
|
|
|
|
使用RQData获取期货数据。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
self.api_connected = False
|
|
|
|
|
|
|
|
|
|
|
|
def connect(self) -> bool:
|
|
|
|
|
|
"""连接API
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
bool: 连接是否成功
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
if RQDATA_AVAILABLE:
|
|
|
|
|
|
# 使用RQData连接
|
|
|
|
|
|
username = os.getenv('RQDATA_USERNAME', '')
|
|
|
|
|
|
password = os.getenv('RQDATA_PASSWORD', '')
|
|
|
|
|
|
|
|
|
|
|
|
if username and password:
|
|
|
|
|
|
rqd.init(username, password)
|
|
|
|
|
|
print("RQData API连接成功")
|
|
|
|
|
|
self.api_connected = True
|
|
|
|
|
|
return True
|
|
|
|
|
|
else:
|
|
|
|
|
|
print("RQData账号密码未配置,将使用模拟数据")
|
|
|
|
|
|
self.api_connected = False
|
|
|
|
|
|
return False
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 模拟API,用于测试
|
|
|
|
|
|
print("RQData不可用,使用模拟API")
|
|
|
|
|
|
self.api_connected = False
|
|
|
|
|
|
return False
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"RQData API连接失败:{e}")
|
|
|
|
|
|
# 模拟API,用于测试
|
|
|
|
|
|
self.api_connected = False
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def disconnect(self):
|
|
|
|
|
|
"""断开连接"""
|
|
|
|
|
|
if self.api_connected:
|
|
|
|
|
|
try:
|
|
|
|
|
|
# RQData不需要显式断开连接
|
|
|
|
|
|
print("RQData API连接已断开")
|
|
|
|
|
|
self.api_connected = False
|
|
|
|
|
|
except:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
def _convert_duration(self, duration: str) -> str:
|
|
|
|
|
|
"""将时间周期字符串转换为RQData格式
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
duration: 时间周期,如 '1m', '5m', '15m', '1h', '1d'
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
RQData格式的时间周期
|
|
|
|
|
|
"""
|
|
|
|
|
|
duration_map = {
|
|
|
|
|
|
'1m': '1m',
|
|
|
|
|
|
'5m': '5m',
|
|
|
|
|
|
'15m': '15m',
|
|
|
|
|
|
'30m': '30m',
|
|
|
|
|
|
'1h': '60m',
|
|
|
|
|
|
'2h': '120m',
|
|
|
|
|
|
'4h': '240m',
|
|
|
|
|
|
'6h': '360m',
|
|
|
|
|
|
'12h': '720m',
|
|
|
|
|
|
'1d': '1d',
|
|
|
|
|
|
'1w': '1w'
|
|
|
|
|
|
}
|
|
|
|
|
|
return duration_map.get(duration, '60m') # 默认60分钟
|
|
|
|
|
|
|
|
|
|
|
|
def _convert_symbol(self, symbol: str) -> str:
|
|
|
|
|
|
"""将合约代码转换为RQData格式
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
symbol: 合约代码,如 'CU2603'
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
RQData格式的合约代码,如 'SHFE.CU2603'
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 交易所映射
|
|
|
|
|
|
exchange_map = {
|
|
|
|
|
|
'CU': 'SHFE', # 铜 - 上海期货交易所
|
|
|
|
|
|
'AL': 'SHFE', # 铝 - 上海期货交易所
|
|
|
|
|
|
'ZN': 'SHFE', # 锌 - 上海期货交易所
|
|
|
|
|
|
'PB': 'SHFE', # 铅 - 上海期货交易所
|
|
|
|
|
|
'NI': 'SHFE', # 镍 - 上海期货交易所
|
|
|
|
|
|
'SN': 'SHFE', # 锡 - 上海期货交易所
|
|
|
|
|
|
'AU': 'SHFE', # 黄金 - 上海期货交易所
|
|
|
|
|
|
'AG': 'SHFE', # 白银 - 上海期货交易所
|
|
|
|
|
|
'RB': 'SHFE', # 螺纹钢 - 上海期货交易所
|
|
|
|
|
|
'HC': 'SHFE', # 热轧卷板 - 上海期货交易所
|
|
|
|
|
|
'BU': 'SHFE', # 沥青 - 上海期货交易所
|
|
|
|
|
|
'RU': 'SHFE', # 橡胶 - 上海期货交易所
|
|
|
|
|
|
'FU': 'SHFE', # 燃油 - 上海期货交易所
|
|
|
|
|
|
'SC': 'INE', # 原油 - 上海国际能源交易中心
|
|
|
|
|
|
'I': 'DCE', # 铁矿石 - 大连商品交易所
|
|
|
|
|
|
'J': 'DCE', # 焦炭 - 大连商品交易所
|
|
|
|
|
|
'JM': 'DCE', # 焦煤 - 大连商品交易所
|
|
|
|
|
|
'A': 'DCE', # 大豆 - 大连商品交易所
|
|
|
|
|
|
'B': 'DCE', # 豆粕 - 大连商品交易所
|
|
|
|
|
|
'M': 'DCE', # 豆粕 - 大连商品交易所
|
|
|
|
|
|
'Y': 'DCE', # 豆油 - 大连商品交易所
|
|
|
|
|
|
'P': 'DCE', # 棕榈油 - 大连商品交易所
|
|
|
|
|
|
'C': 'DCE', # 玉米 - 大连商品交易所
|
|
|
|
|
|
'CS': 'DCE', # 玉米淀粉 - 大连商品交易所
|
|
|
|
|
|
'L': 'DCE', # 聚乙烯 - 大连商品交易所
|
|
|
|
|
|
'V': 'DCE', # 聚氯乙烯 - 大连商品交易所
|
|
|
|
|
|
'PP': 'DCE', # 聚丙烯 - 大连商品交易所
|
|
|
|
|
|
'TA': 'CZCE', # PTA - 郑州商品交易所
|
|
|
|
|
|
'CF': 'CZCE', # 棉花 - 郑州商品交易所
|
|
|
|
|
|
'SR': 'CZCE', # 白糖 - 郑州商品交易所
|
|
|
|
|
|
'MA': 'CZCE', # 甲醇 - 郑州商品交易所
|
|
|
|
|
|
'ZC': 'CZCE', # 动力煤 - 郑州商品交易所
|
|
|
|
|
|
'FG': 'CZCE', # 玻璃 - 郑州商品交易所
|
|
|
|
|
|
'RM': 'CZCE', # 菜籽粕 - 郑州商品交易所
|
|
|
|
|
|
'OI': 'CZCE', # 菜籽油 - 郑州商品交易所
|
|
|
|
|
|
'RS': 'CZCE', # 菜籽 - 郑州商品交易所
|
|
|
|
|
|
'WH': 'CZCE', # 强麦 - 郑州商品交易所
|
|
|
|
|
|
'JR': 'CZCE', # 粳稻 - 郑州商品交易所
|
|
|
|
|
|
'LR': 'CZCE', # 晚籼稻 - 郑州商品交易所
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 提取品种代码和合约月份
|
|
|
|
|
|
if len(symbol) >= 4:
|
|
|
|
|
|
product_code = symbol[:2].upper()
|
|
|
|
|
|
contract_month = symbol[2:].upper()
|
|
|
|
|
|
|
|
|
|
|
|
# 获取交易所代码
|
|
|
|
|
|
exchange = exchange_map.get(product_code, 'SHFE')
|
|
|
|
|
|
|
|
|
|
|
|
# 构建RQData格式的合约代码
|
|
|
|
|
|
rq_symbol = f"{exchange}.{product_code}{contract_month}"
|
|
|
|
|
|
return rq_symbol
|
|
|
|
|
|
else:
|
|
|
|
|
|
return symbol
|
|
|
|
|
|
|
|
|
|
|
|
def get_kline_data(self, symbol: str, duration: str, count: int = 200) -> Optional[pd.DataFrame]:
|
|
|
|
|
|
"""获取K线数据
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
symbol: 合约代码
|
|
|
|
|
|
duration: 时间周期,如 '1m', '5m', '15m', '1h', '1d'
|
|
|
|
|
|
count: 数据数量
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
K线数据DataFrame,如果无法获取真实数据则返回None
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
if RQDATA_AVAILABLE and self.api_connected:
|
|
|
|
|
|
# 转换合约代码为RQData格式
|
|
|
|
|
|
rq_symbol = self._convert_symbol(symbol)
|
|
|
|
|
|
print(f"使用RQData格式合约代码: {rq_symbol}")
|
|
|
|
|
|
|
|
|
|
|
|
# 转换时间周期为RQData格式
|
|
|
|
|
|
rq_duration = self._convert_duration(duration)
|
|
|
|
|
|
|
|
|
|
|
|
# 计算开始时间
|
|
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
|
end_date = datetime.now()
|
|
|
|
|
|
|
|
|
|
|
|
# 根据时间周期计算开始日期
|
|
|
|
|
|
if rq_duration == '1d':
|
|
|
|
|
|
start_date = end_date - timedelta(days=count)
|
|
|
|
|
|
elif rq_duration == '1w':
|
|
|
|
|
|
start_date = end_date - timedelta(weeks=count)
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 对于分钟级别,计算大致的天数
|
|
|
|
|
|
minutes_per_period = int(rq_duration[:-1])
|
|
|
|
|
|
total_minutes = minutes_per_period * count
|
|
|
|
|
|
start_date = end_date - timedelta(minutes=total_minutes)
|
|
|
|
|
|
|
|
|
|
|
|
# 使用RQData获取K线数据
|
|
|
|
|
|
df = rqd.get_price(
|
|
|
|
|
|
rq_symbol,
|
|
|
|
|
|
start_date=start_date,
|
|
|
|
|
|
end_date=end_date,
|
|
|
|
|
|
frequency=rq_duration,
|
|
|
|
|
|
fields=['open', 'high', 'low', 'close', 'volume', 'open_interest'],
|
|
|
|
|
|
adjust_type='none'
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if not df.empty:
|
|
|
|
|
|
print(f"成功获取K线数据,数据长度: {len(df)}")
|
|
|
|
|
|
return df
|
|
|
|
|
|
else:
|
|
|
|
|
|
print("获取K线数据失败:无数据返回")
|
|
|
|
|
|
return None
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 不再自动返回模拟数据,返回None
|
|
|
|
|
|
print(f"无法获取真实数据:{'API未连接' if not self.api_connected else 'RQData不可用'}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"获取K线数据失败:{e}")
|
|
|
|
|
|
# 不再自动返回模拟数据,返回None
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def get_tick_data(self, symbol: str, count: int = 1000) -> Optional[pd.DataFrame]:
|
|
|
|
|
|
"""获取Tick数据"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
if RQDATA_AVAILABLE and self.api_connected:
|
|
|
|
|
|
# 转换合约代码为RQData格式
|
|
|
|
|
|
rq_symbol = self._convert_symbol(symbol)
|
|
|
|
|
|
print(f"使用RQData格式合约代码: {rq_symbol}")
|
|
|
|
|
|
|
|
|
|
|
|
# 计算开始时间
|
|
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
|
end_date = datetime.now()
|
|
|
|
|
|
start_date = end_date - timedelta(days=1) # RQData Tick数据通常只能获取最近1天
|
|
|
|
|
|
|
|
|
|
|
|
# 使用RQData获取Tick数据
|
|
|
|
|
|
df = rqd.get_price(
|
|
|
|
|
|
rq_symbol,
|
|
|
|
|
|
start_date=start_date,
|
|
|
|
|
|
end_date=end_date,
|
|
|
|
|
|
frequency='tick',
|
|
|
|
|
|
fields=['last', 'volume', 'open_interest', 'bid_price1', 'bid_volume1', 'ask_price1', 'ask_volume1'],
|
|
|
|
|
|
adjust_type='none'
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if not df.empty:
|
|
|
|
|
|
# 重命名列以保持与原来的接口一致
|
|
|
|
|
|
df = df.rename(columns={
|
|
|
|
|
|
'last': 'last_price',
|
|
|
|
|
|
'bid_price1': 'bid_price1',
|
|
|
|
|
|
'bid_volume1': 'bid_volume1',
|
|
|
|
|
|
'ask_price1': 'ask_price1',
|
|
|
|
|
|
'ask_volume1': 'ask_volume1'
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
print(f"成功获取Tick数据,数据长度: {len(df)}")
|
|
|
|
|
|
return df.tail(count) # 只返回最近的count条数据
|
|
|
|
|
|
else:
|
|
|
|
|
|
print("获取Tick数据失败:无数据返回")
|
|
|
|
|
|
return None
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 返回模拟数据
|
|
|
|
|
|
print(f"无法获取真实数据:{'API未连接' if not self.api_connected else 'RQData不可用'}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"获取Tick数据失败:{e}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def get_contract_info(self, symbol: str) -> Optional[Dict]:
|
|
|
|
|
|
"""获取合约信息"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
if RQDATA_AVAILABLE and self.api_connected:
|
|
|
|
|
|
# 转换合约代码为RQData格式
|
|
|
|
|
|
rq_symbol = self._convert_symbol(symbol)
|
|
|
|
|
|
print(f"使用RQData格式合约代码: {rq_symbol}")
|
|
|
|
|
|
|
|
|
|
|
|
# 使用RQData获取合约信息
|
|
|
|
|
|
instrument = rqd.instruments(rq_symbol)
|
|
|
|
|
|
|
|
|
|
|
|
if instrument:
|
|
|
|
|
|
return {
|
|
|
|
|
|
'symbol': symbol,
|
|
|
|
|
|
'name': instrument[0].name,
|
|
|
|
|
|
'exchange': instrument[0].exchange,
|
|
|
|
|
|
'product': instrument[0].underlying_symbol,
|
|
|
|
|
|
'price_tick': instrument[0].price_tick,
|
|
|
|
|
|
'volume_multiple': instrument[0].contract_multiplier,
|
|
|
|
|
|
'margin_rate': instrument[0].margin_rate,
|
|
|
|
|
|
'expire_datetime': instrument[0].maturity_date,
|
|
|
|
|
|
'create_datetime': instrument[0].listed_date
|
|
|
|
|
|
}
|
|
|
|
|
|
else:
|
|
|
|
|
|
print("获取合约信息失败:合约不存在")
|
|
|
|
|
|
return None
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 返回模拟数据
|
|
|
|
|
|
print(f"无法获取真实数据:{'API未连接' if not self.api_connected else 'RQData不可用'}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"获取合约信息失败:{e}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def get_market_data(self, symbols: List[str]) -> Dict[str, Dict]:
|
|
|
|
|
|
"""批量获取市场数据"""
|
|
|
|
|
|
market_data = {}
|
|
|
|
|
|
|
|
|
|
|
|
for symbol in symbols:
|
|
|
|
|
|
try:
|
|
|
|
|
|
if RQDATA_AVAILABLE and self.api_connected:
|
|
|
|
|
|
# 转换合约代码为RQData格式
|
|
|
|
|
|
rq_symbol = self._convert_symbol(symbol)
|
|
|
|
|
|
print(f"使用RQData格式合约代码: {rq_symbol}")
|
|
|
|
|
|
|
|
|
|
|
|
# 使用RQData获取最新行情数据
|
|
|
|
|
|
quote = rqd.get_quote(rq_symbol)
|
|
|
|
|
|
|
|
|
|
|
|
if not quote.empty:
|
|
|
|
|
|
market_data[symbol] = {
|
|
|
|
|
|
'latest_price': quote['last'].iloc[0],
|
|
|
|
|
|
'open': quote['open'].iloc[0],
|
|
|
|
|
|
'high': quote['high'].iloc[0],
|
|
|
|
|
|
'low': quote['low'].iloc[0],
|
|
|
|
|
|
'pre_close': quote['prev_close'].iloc[0],
|
|
|
|
|
|
'volume': quote['volume'].iloc[0],
|
|
|
|
|
|
'open_interest': quote['open_interest'].iloc[0],
|
|
|
|
|
|
'bid_price1': quote['bid1'].iloc[0],
|
|
|
|
|
|
'ask_price1': quote['ask1'].iloc[0]
|
|
|
|
|
|
}
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"获取{symbol}市场数据失败:无数据返回")
|
|
|
|
|
|
market_data[symbol] = {
|
|
|
|
|
|
'latest_price': 0,
|
|
|
|
|
|
'open': 0,
|
|
|
|
|
|
'high': 0,
|
|
|
|
|
|
'low': 0,
|
|
|
|
|
|
'pre_close': 0,
|
|
|
|
|
|
'volume': 0,
|
|
|
|
|
|
'open_interest': 0,
|
|
|
|
|
|
'bid_price1': 0,
|
|
|
|
|
|
'ask_price1': 0
|
|
|
|
|
|
}
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 模拟数据
|
|
|
|
|
|
market_data[symbol] = {
|
|
|
|
|
|
'latest_price': 0,
|
|
|
|
|
|
'open': 0,
|
|
|
|
|
|
'high': 0,
|
|
|
|
|
|
'low': 0,
|
|
|
|
|
|
'pre_close': 0,
|
|
|
|
|
|
'volume': 0,
|
|
|
|
|
|
'open_interest': 0,
|
|
|
|
|
|
'bid_price1': 0,
|
|
|
|
|
|
'ask_price1': 0
|
|
|
|
|
|
}
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"获取{symbol}市场数据失败:{e}")
|
|
|
|
|
|
market_data[symbol] = {
|
|
|
|
|
|
'latest_price': 0,
|
|
|
|
|
|
'open': 0,
|
|
|
|
|
|
'high': 0,
|
|
|
|
|
|
'low': 0,
|
|
|
|
|
|
'pre_close': 0,
|
|
|
|
|
|
'volume': 0,
|
|
|
|
|
|
'open_interest': 0,
|
|
|
|
|
|
'bid_price1': 0,
|
|
|
|
|
|
'ask_price1': 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return market_data
|
|
|
|
|
|
|
|
|
|
|
|
def get_all_symbols(self) -> List[str]:
|
|
|
|
|
|
"""获取所有品种列表
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
List[str]: 所有品种的合约代码列表
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 直接使用本地枚举数据,不使用RQData获取
|
|
|
|
|
|
print("使用本地枚举品种列表")
|
|
|
|
|
|
# 从get_all_symbols_by_exchange获取所有品种
|
|
|
|
|
|
from qihuo_analyzer.data.data_fetcher import DataFetcher
|
|
|
|
|
|
data_fetcher = DataFetcher()
|
|
|
|
|
|
symbols_by_exchange = data_fetcher.get_all_symbols_by_exchange()
|
|
|
|
|
|
symbols = []
|
|
|
|
|
|
for exchange, products in symbols_by_exchange.items():
|
|
|
|
|
|
for product, product_data in products.items():
|
|
|
|
|
|
# 使用每个品种的第一个合约作为代表
|
|
|
|
|
|
if product_data['contracts']:
|
|
|
|
|
|
symbols.append(product_data['contracts'][0])
|
|
|
|
|
|
return symbols
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"获取所有品种列表失败:{e}")
|
|
|
|
|
|
# 返回模拟数据
|
|
|
|
|
|
return [
|
|
|
|
|
|
"CU2603", "AL2603", "ZN2603", "PB2603", "NI2603", "SN2603",
|
|
|
|
|
|
"AU2603", "AG2603", "RB2603", "HC2603", "BU2603", "RU2603",
|
|
|
|
|
|
"SC2603", "I2603", "J2603", "JM2603", "A2603", "M2603",
|
|
|
|
|
|
"Y2603", "P2603", "C2603", "CS2603", "L2603", "V2603",
|
|
|
|
|
|
"PP2603", "TA2603", "CF2603", "SR2603", "MA2603", "FG2603"
|
|
|
|
|
|
]
|