diff --git a/# AI 期货分析系统运行指南.ini b/# AI 期货分析系统运行指南.ini index 178012a..6e1ddde 100644 --- a/# AI 期货分析系统运行指南.ini +++ b/# AI 期货分析系统运行指南.ini @@ -21,7 +21,7 @@ pip --version pip install -r requirements.txt ``` ### 2.2 解决可能的依赖问题 -- tqsdk 导入失败 : 系统会自动使用模拟数据,不影响基本功能 +- RQData 导入失败 : 系统会自动使用模拟数据,不影响基本功能 - TA-Lib 安装失败 : 可以注释掉该依赖,系统会使用内置的技术分析函数 ## 3. 配置设置 ### 3.1 创建环境配置文件 diff --git a/.env b/.env index deb3ca8..3bf022c 100644 --- a/.env +++ b/.env @@ -15,6 +15,10 @@ TQSDK_PASSWORD=1qazse42W3 TQSERVER_HOST=api.shinnytech.com TQSERVER_PORT=7777 +#米筐RQData配置 +RQDATA_USERNAME=18600025116 +RQDATA_PASSWORD=1qazse42W3 + # 数据库配置 DB_PATH=./data/futures_analysis.db diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..e09d796 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,18 @@ +--- +name: AlphaFutures +description: A brief description of what this skill does +--- + +# AlphaFutures + +Instructions for the agent to follow when this skill is activated. + +## When to use + +Describe when this skill should be used. + +## Instructions + +1. First step +2. Second step +3. Additional steps as needed diff --git a/__pycache__/config.cpython-311.pyc b/__pycache__/config.cpython-311.pyc new file mode 100644 index 0000000..ec66a49 Binary files /dev/null and b/__pycache__/config.cpython-311.pyc differ diff --git a/config.py b/config.py new file mode 100644 index 0000000..e8ebcf5 --- /dev/null +++ b/config.py @@ -0,0 +1,29 @@ +# 数据源配置文件 +# 统一数据获取接口的配置参数 + +# 数据源类型配置 +# 可选值: tqsdk, rqdata +DATA_ADAPTER_TYPE = "tqsdk" + +# TQSDK账号配置(可选) +# 未配置时会使用模拟数据 +TQSDK_USERNAME = "windsdreamer" +TQSDK_PASSWORD = "1qazse42W3" + +# RQData账号配置(可选) +# 未配置时会使用模拟数据 +RQDATA_USERNAME = "18600025116" +RQDATA_PASSWORD = "1qazse42W3" + +# 调试模式配置 +# 设置为True时启用详细日志 +DEBUG = False + +# 数据源连接超时设置(秒) +CONNECTION_TIMEOUT = 30 + +# 数据缓存配置 +# 设置为True时启用数据缓存,提高重复查询性能 +ENABLE_CACHE = True +# 缓存过期时间(秒) +CACHE_EXPIRY = 3600 diff --git a/qihuo_analyzer/data/__pycache__/data_fetcher.cpython-311.pyc b/qihuo_analyzer/data/__pycache__/data_fetcher.cpython-311.pyc index f689264..1d3906c 100644 Binary files a/qihuo_analyzer/data/__pycache__/data_fetcher.cpython-311.pyc and b/qihuo_analyzer/data/__pycache__/data_fetcher.cpython-311.pyc differ diff --git a/qihuo_analyzer/data/api_adapters/__init__.py b/qihuo_analyzer/data/api_adapters/__init__.py new file mode 100644 index 0000000..849d951 --- /dev/null +++ b/qihuo_analyzer/data/api_adapters/__init__.py @@ -0,0 +1,12 @@ +# API适配器包初始化文件 +from qihuo_analyzer.data.api_adapters.base_adapter import BaseDataAdapter +from qihuo_analyzer.data.api_adapters.tqsdk_adapter import TqSdkAdapter +from qihuo_analyzer.data.api_adapters.rqdata_adapter import RqDataAdapter +from qihuo_analyzer.data.api_adapters.adapter_factory import DataAdapterFactory + +__all__ = [ + 'BaseDataAdapter', + 'TqSdkAdapter', + 'RqDataAdapter', + 'DataAdapterFactory' +] diff --git a/qihuo_analyzer/data/api_adapters/__pycache__/__init__.cpython-311.pyc b/qihuo_analyzer/data/api_adapters/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..69eba42 Binary files /dev/null and b/qihuo_analyzer/data/api_adapters/__pycache__/__init__.cpython-311.pyc differ diff --git a/qihuo_analyzer/data/api_adapters/__pycache__/adapter_factory.cpython-311.pyc b/qihuo_analyzer/data/api_adapters/__pycache__/adapter_factory.cpython-311.pyc new file mode 100644 index 0000000..d702cdc Binary files /dev/null and b/qihuo_analyzer/data/api_adapters/__pycache__/adapter_factory.cpython-311.pyc differ diff --git a/qihuo_analyzer/data/api_adapters/__pycache__/base_adapter.cpython-311.pyc b/qihuo_analyzer/data/api_adapters/__pycache__/base_adapter.cpython-311.pyc new file mode 100644 index 0000000..0872ab0 Binary files /dev/null and b/qihuo_analyzer/data/api_adapters/__pycache__/base_adapter.cpython-311.pyc differ diff --git a/qihuo_analyzer/data/api_adapters/__pycache__/rqdata_adapter.cpython-311.pyc b/qihuo_analyzer/data/api_adapters/__pycache__/rqdata_adapter.cpython-311.pyc new file mode 100644 index 0000000..3ee3549 Binary files /dev/null and b/qihuo_analyzer/data/api_adapters/__pycache__/rqdata_adapter.cpython-311.pyc differ diff --git a/qihuo_analyzer/data/api_adapters/__pycache__/tqsdk_adapter.cpython-311.pyc b/qihuo_analyzer/data/api_adapters/__pycache__/tqsdk_adapter.cpython-311.pyc new file mode 100644 index 0000000..64769aa Binary files /dev/null and b/qihuo_analyzer/data/api_adapters/__pycache__/tqsdk_adapter.cpython-311.pyc differ diff --git a/qihuo_analyzer/data/api_adapters/adapter_factory.py b/qihuo_analyzer/data/api_adapters/adapter_factory.py new file mode 100644 index 0000000..61bda41 --- /dev/null +++ b/qihuo_analyzer/data/api_adapters/adapter_factory.py @@ -0,0 +1,38 @@ +# 适配器工厂类 +from qihuo_analyzer.data.api_adapters.base_adapter import BaseDataAdapter +from qihuo_analyzer.data.api_adapters.tqsdk_adapter import TqSdkAdapter +from qihuo_analyzer.data.api_adapters.rqdata_adapter import RqDataAdapter +import os + + +class DataAdapterFactory: + """数据适配器工厂类 + + 根据配置创建相应的数据适配器实例。 + """ + + @staticmethod + def create_adapter(adapter_type: str = None) -> BaseDataAdapter: + """创建数据适配器 + + Args: + adapter_type: 适配器类型,可选值:'tqsdk', 'rqdata'。如果为None,则从环境变量获取。 + + Returns: + BaseDataAdapter: 数据适配器实例 + """ + # 如果没有指定适配器类型,从环境变量获取 + if adapter_type is None: + adapter_type = os.getenv('DATA_ADAPTER_TYPE', 'tqsdk').lower() + + # 根据类型创建适配器 + if adapter_type == 'tqsdk': + print("创建TQSDK数据适配器") + return TqSdkAdapter() + elif adapter_type == 'rqdata': + print("创建RQData数据适配器") + return RqDataAdapter() + else: + # 默认使用TQSDK适配器 + print(f"未知的适配器类型:{adapter_type},使用默认的TQSDK适配器") + return TqSdkAdapter() diff --git a/qihuo_analyzer/data/api_adapters/base_adapter.py b/qihuo_analyzer/data/api_adapters/base_adapter.py new file mode 100644 index 0000000..df6d490 --- /dev/null +++ b/qihuo_analyzer/data/api_adapters/base_adapter.py @@ -0,0 +1,85 @@ +# 数据获取适配器基类 +from abc import ABC, abstractmethod +from typing import Dict, Optional, List +import pandas as pd + + +class BaseDataAdapter(ABC): + """数据获取适配器基类 + + 所有数据获取适配器都需要实现这个接口,确保统一的方法调用方式。 + """ + + @abstractmethod + def connect(self) -> bool: + """连接API + + Returns: + bool: 连接是否成功 + """ + pass + + @abstractmethod + def disconnect(self): + """断开连接""" + pass + + @abstractmethod + 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 + """ + pass + + @abstractmethod + def get_tick_data(self, symbol: str, count: int = 1000) -> Optional[pd.DataFrame]: + """获取Tick数据 + + Args: + symbol: 合约代码 + count: 数据数量 + + Returns: + Tick数据DataFrame,如果无法获取真实数据则返回None + """ + pass + + @abstractmethod + def get_contract_info(self, symbol: str) -> Optional[Dict]: + """获取合约信息 + + Args: + symbol: 合约代码 + + Returns: + 合约信息字典,如果无法获取真实数据则返回None + """ + pass + + @abstractmethod + def get_market_data(self, symbols: List[str]) -> Dict[str, Dict]: + """批量获取市场数据 + + Args: + symbols: 合约代码列表 + + Returns: + 市场数据字典,键为合约代码,值为市场数据 + """ + pass + + @abstractmethod + def get_all_symbols(self) -> List[str]: + """获取所有品种列表 + + Returns: + 所有品种的合约代码列表 + """ + pass diff --git a/qihuo_analyzer/data/api_adapters/rqdata_adapter.py b/qihuo_analyzer/data/api_adapters/rqdata_adapter.py new file mode 100644 index 0000000..2977c1e --- /dev/null +++ b/qihuo_analyzer/data/api_adapters/rqdata_adapter.py @@ -0,0 +1,396 @@ +# 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" + ] diff --git a/qihuo_analyzer/data/api_adapters/tqsdk_adapter.py b/qihuo_analyzer/data/api_adapters/tqsdk_adapter.py new file mode 100644 index 0000000..db40209 --- /dev/null +++ b/qihuo_analyzer/data/api_adapters/tqsdk_adapter.py @@ -0,0 +1,335 @@ +# TQSDK数据适配器 +import os +import time +import pandas as pd +from typing import Dict, Optional, List +from qihuo_analyzer.data.api_adapters.base_adapter import BaseDataAdapter + +# 尝试导入tqsdk + +try: + from tqsdk import TqApi, TqAuth + TQSDK_AVAILABLE = True +except Exception as e: + print(f"tqsdk导入失败:{e},将使用模拟数据") + TQSDK_AVAILABLE = False + + +class TqSdkAdapter(BaseDataAdapter): + """TQSDK数据适配器 + + 使用天勤TQSDK获取期货数据。 + """ + + def __init__(self): + self.api = None + # 交易所映射 + self.exchange_map = { + 'AU': 'SHFE', # 黄金 - 上海期货交易所 + 'AG': 'SHFE', # 白银 - 上海期货交易所 + 'CU': 'SHFE', # 铜 - 上海期货交易所 + 'NI': 'SHFE', # 镍 - 上海期货交易所 + 'SN': 'SHFE', # 锡 - 上海期货交易所 + } + + def connect(self) -> bool: + """连接API + + Returns: + bool: 连接是否成功 + """ + try: + if TQSDK_AVAILABLE: + # 使用天勤TQSDK连接 + username = os.getenv('TQSDK_USERNAME', '') + password = os.getenv('TQSDK_PASSWORD', '') + + if username and password: + self.api = TqApi(auth=TqAuth(username, password)) + print("TQSDK API连接成功") + return True + else: + print("TQSDK账号密码未配置,将使用模拟数据") + self.api = None + return False + else: + # 模拟API,用于测试 + print("TQSDK不可用,使用模拟API") + self.api = None + return False + except Exception as e: + print(f"TQSDK API连接失败:{e}") + # 模拟API,用于测试 + self.api = None + return False + + def disconnect(self): + """断开连接""" + if self.api: + try: + self.api.close() + print("TQSDK API连接已断开") + except: + pass + + def _convert_duration(self, duration: str) -> int: + """将时间周期字符串转换为分钟数 + + Args: + duration: 时间周期,如 '1m', '5m', '15m', '1h', '1d' + + Returns: + 分钟数 + """ + duration_map = { + '1m': 1, + '5m': 5, + '15m': 15, + '30m': 30, + '1h': 60, + '2h': 120, + '4h': 240, + '6h': 360, + '12h': 720, + '1d': 1440, + '1w': 10080 + } + return duration_map.get(duration, 60) # 默认60分钟 + + def _convert_symbol(self, symbol: str) -> str: + """将合约代码转换为TQSDK格式 + + Args: + symbol: 合约代码,如 'CU2603' + + Returns: + TQSDK格式的合约代码,如 'SHFE.cu2603' + """ + # 提取品种代码和合约月份 + if len(symbol) >= 4: + # 3字符品种代码 + if len(symbol) >= 5: + product_code = symbol[:3].upper() + if product_code in self.exchange_map: + contract_month = symbol[3:].lower() + exchange = self.exchange_map[product_code] + return f"{exchange}.{product_code.lower()}{contract_month}" + + # 2字符品种代码 + product_code = symbol[:2].upper() + if product_code in self.exchange_map: + contract_month = symbol[2:].lower() + exchange = self.exchange_map[product_code] + return f"{exchange}.{product_code.lower()}{contract_month}" + + # 1字符品种代码 + product_code = symbol[:1].upper() + if product_code in self.exchange_map: + contract_month = symbol[1:].lower() + exchange = self.exchange_map[product_code] + return f"{exchange}.{product_code.lower()}{contract_month}" + + # 无法识别的合约代码,返回原始代码 + return 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 TQSDK_AVAILABLE and self.api: + # 转换合约代码为TQSDK格式 + tq_symbol = self._convert_symbol(symbol) + print(f"使用TQSDK格式合约代码: {tq_symbol}") + + # 转换时间周期为分钟数 + duration_minutes = self._convert_duration(duration) + # 使用真实API获取数据 + klines = self.api.get_kline_serial(tq_symbol, duration_minutes, data_length=count) + + # 等待数据准备就绪 + import time + start_time = time.time() + timeout = 5 # 5秒超时 + + while True: + if hasattr(klines, 'datetime') and len(klines.datetime) > 0: + break + if time.time() - start_time > timeout: + print("获取K线数据超时") + return None + time.sleep(0.1) + + # 转换为DataFrame + data = { + 'datetime': klines.datetime, + 'open': klines.open, + 'high': klines.high, + 'low': klines.low, + 'close': klines.close, + 'volume': klines.volume, + 'open_interest': klines.open_oi + } + df = pd.DataFrame(data) + df['datetime'] = pd.to_datetime(df['datetime'], unit='ns') + df.set_index('datetime', inplace=True) + + print(f"成功获取K线数据,数据长度: {len(df)}") + return df + else: + # 不再自动返回模拟数据,返回None + print(f"无法获取真实数据:{'API未连接' if not self.api else 'TQSDK不可用'}") + 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 TQSDK_AVAILABLE and self.api: + # 使用真实API获取数据 + ticks = self.api.get_tick_serial(symbol, data_length=count) + self.api.wait_update() + + # 转换为DataFrame + data = { + 'datetime': ticks.datetime, + 'last_price': ticks.last_price, + 'volume': ticks.volume, + 'open_interest': ticks.open_interest, + 'bid_price1': ticks.bid_price1, + 'bid_volume1': ticks.bid_volume1, + 'ask_price1': ticks.ask_price1, + 'ask_volume1': ticks.ask_volume1 + } + df = pd.DataFrame(data) + df['datetime'] = pd.to_datetime(df['datetime'], unit='ns') + df.set_index('datetime', inplace=True) + + return df + else: + # 返回模拟数据 + print(f"无法获取真实数据:{'API未连接' if not self.api else 'TQSDK不可用'}") + return None + except Exception as e: + print(f"获取Tick数据失败:{e}") + return None + + def get_contract_info(self, symbol: str) -> Optional[Dict]: + """获取合约信息""" + try: + if TQSDK_AVAILABLE and self.api: + # 使用真实API获取数据 + quote = self.api.get_quote(symbol) + self.api.wait_update() + + return { + 'symbol': symbol, + 'name': quote.instrument_name, + 'exchange': quote.exchange_id, + 'product': quote.product_id, + 'price_tick': quote.price_tick, + 'volume_multiple': quote.volume_multiple, + 'margin_rate': quote.margin_rate, + 'expire_datetime': quote.expire_datetime, + 'create_datetime': quote.create_datetime + } + else: + # 返回模拟数据 + print(f"无法获取真实数据:{'API未连接' if not self.api else 'TQSDK不可用'}") + 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 TQSDK_AVAILABLE and self.api: + quote = self.api.get_quote(symbol) + self.api.wait_update() + + market_data[symbol] = { + 'latest_price': quote.last_price, + 'open': quote.open, + 'high': quote.high, + 'low': quote.low, + 'pre_close': quote.pre_close, + 'volume': quote.volume, + 'open_interest': quote.open_interest, + 'bid_price1': quote.bid_price1, + 'ask_price1': quote.ask_price1 + } + 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: + if TQSDK_AVAILABLE and self.api: + # TQSDK 没有直接获取所有品种列表的方法,使用模拟数据 + print("TQSDK 不支持获取所有品种列表,使用模拟数据") + return self._get_mock_all_symbols() + else: + # 返回模拟数据 + print("使用模拟品种列表") + return self._get_mock_all_symbols() + except Exception as e: + print(f"获取所有品种列表失败:{e}") + return self._get_mock_all_symbols() + + def _get_mock_all_symbols(self) -> List[str]: + """获取模拟品种列表""" + # 返回exchange_map中映射的所有品种 + symbols = [] + # 为每个品种生成一个合约代码(使用2603月份) + for product_code in self.exchange_map: + # 生成合约代码,格式:品种代码+2603 + contract_code = f"{product_code}2603" + symbols.append(contract_code) + print(f"模拟品种列表: {symbols}") + return symbols diff --git a/qihuo_analyzer/data/data_fetcher.py b/qihuo_analyzer/data/data_fetcher.py index 2d791dc..4425b74 100644 --- a/qihuo_analyzer/data/data_fetcher.py +++ b/qihuo_analyzer/data/data_fetcher.py @@ -4,148 +4,40 @@ import time import pandas as pd from typing import Dict, Optional, List from qihuo_analyzer.utils.config_manager import config_manager - -# 尝试导入tqsdk,如果失败则使用模拟数据 -try: - from tqsdk import TqApi, TqAuth - TQSDK_AVAILABLE = True -except Exception as e: - print(f"tqsdk导入失败:{e},将使用模拟数据") - TQSDK_AVAILABLE = False +from qihuo_analyzer.data.api_adapters import DataAdapterFactory class DataFetcher: """数据获取器""" def __init__(self): - self.api = None + # 使用适配器工厂创建数据适配器 + self.adapter = DataAdapterFactory.create_adapter() + self.api_connected = False def connect(self) -> bool: """连接API""" try: - if TQSDK_AVAILABLE: - # 使用天勤TQSDK连接 - from qihuo_analyzer.utils.config_manager import config_manager - username = os.getenv('TQSDK_USERNAME', '') - password = os.getenv('TQSDK_PASSWORD', '') - - if username and password: - self.api = TqApi(auth=TqAuth(username, password)) - print("API连接成功") - return True - else: - print("TQSDK账号密码未配置,将使用模拟数据") - self.api = None - return False - else: - # 模拟API,用于测试 - print("使用模拟API") - self.api = None - return False + # 使用适配器的connect方法 + success = self.adapter.connect() + self.api_connected = success + return success except Exception as e: print(f"API连接失败:{e}") - # 模拟API,用于测试 - self.api = None + self.api_connected = False return False def disconnect(self): """断开连接""" - if self.api: + if self.api_connected: try: - self.api.close() - print("API连接已断开") + # 使用适配器的disconnect方法 + self.adapter.disconnect() + self.api_connected = False except: pass - def _convert_duration(self, duration: str) -> int: - """将时间周期字符串转换为分钟数 - - Args: - duration: 时间周期,如 '1m', '5m', '15m', '1h', '1d' - - Returns: - 分钟数 - """ - duration_map = { - '1m': 1, - '5m': 5, - '15m': 15, - '30m': 30, - '1h': 60, - '2h': 120, - '4h': 240, - '6h': 360, - '12h': 720, - '1d': 1440, - '1w': 10080 - } - return duration_map.get(duration, 60) # 默认60分钟 - - def _convert_symbol(self, symbol: str) -> str: - """将合约代码转换为TQSDK格式 - - Args: - symbol: 合约代码,如 'CU2603' - - Returns: - TQSDK格式的合约代码,如 '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:].lower() - - # 获取交易所代码 - exchange = exchange_map.get(product_code, 'SHFE') - - # 构建TQSDK格式的合约代码 - tq_symbol = f"{exchange}.{product_code.lower()}{contract_month}" - return tq_symbol - else: - return symbol + def get_product_name_cn(self, symbol: str) -> str: """获取合约的中文名称 @@ -214,84 +106,29 @@ class DataFetcher: count: 数据数量 Returns: - K线数据DataFrame,如果无法获取真实数据则返回None + K线数据DataFrame,如果无法获取真实数据则返回模拟数据 """ try: - if TQSDK_AVAILABLE and self.api: - # 转换合约代码为TQSDK格式 - tq_symbol = self._convert_symbol(symbol) - print(f"使用TQSDK格式合约代码: {tq_symbol}") - - # 转换时间周期为分钟数 - duration_minutes = self._convert_duration(duration) - # 使用真实API获取数据 - klines = self.api.get_kline_serial(tq_symbol, duration_minutes, data_length=count) - - # 等待数据准备就绪 - import time - start_time = time.time() - timeout = 5 # 5秒超时 - - while True: - if hasattr(klines, 'datetime') and len(klines.datetime) > 0: - break - if time.time() - start_time > timeout: - print("获取K线数据超时") - return None - time.sleep(0.1) - - # 转换为DataFrame - data = { - 'datetime': klines.datetime, - 'open': klines.open, - 'high': klines.high, - 'low': klines.low, - 'close': klines.close, - 'volume': klines.volume, - 'open_interest': klines.open_oi - } - df = pd.DataFrame(data) - df['datetime'] = pd.to_datetime(df['datetime'], unit='ns') - df.set_index('datetime', inplace=True) - - print(f"成功获取K线数据,数据长度: {len(df)}") - return df - else: - # 不再自动返回模拟数据,返回None - print(f"无法获取真实数据:{'API未连接' if not self.api else 'TQSDK不可用'}") - return None + # 使用适配器的get_kline_data方法 + result = self.adapter.get_kline_data(symbol, duration, count) + if result is None: + # 如果适配器返回None,使用模拟数据 + print("适配器返回None,使用模拟K线数据") + return self._get_mock_kline_data(symbol, duration, count) + return result except Exception as e: print(f"获取K线数据失败:{e}") - # 不再自动返回模拟数据,返回None - return None + return self._get_mock_kline_data(symbol, duration, count) def get_tick_data(self, symbol: str, count: int = 1000) -> Optional[pd.DataFrame]: """获取Tick数据""" try: - if TQSDK_AVAILABLE and self.api: - # 使用真实API获取数据 - ticks = self.api.get_tick_serial(symbol, data_length=count) - self.api.wait_update() - - # 转换为DataFrame - data = { - 'datetime': ticks.datetime, - 'last_price': ticks.last_price, - 'volume': ticks.volume, - 'open_interest': ticks.open_interest, - 'bid_price1': ticks.bid_price1, - 'bid_volume1': ticks.bid_volume1, - 'ask_price1': ticks.ask_price1, - 'ask_volume1': ticks.ask_volume1 - } - df = pd.DataFrame(data) - df['datetime'] = pd.to_datetime(df['datetime'], unit='ns') - df.set_index('datetime', inplace=True) - - return df - else: - # 返回模拟数据 + # 使用适配器的get_tick_data方法 + result = self.adapter.get_tick_data(symbol, count) + if result is None: + # 如果适配器返回None,使用模拟数据 return self._get_mock_tick_data(symbol, count) + return result except Exception as e: print(f"获取Tick数据失败:{e}") return self._get_mock_tick_data(symbol, count) @@ -299,58 +136,36 @@ class DataFetcher: def get_contract_info(self, symbol: str) -> Optional[Dict]: """获取合约信息""" try: - if TQSDK_AVAILABLE and self.api: - # 使用真实API获取数据 - quote = self.api.get_quote(symbol) - self.api.wait_update() - - return { - 'symbol': symbol, - 'name': quote.instrument_name, - 'exchange': quote.exchange_id, - 'product': quote.product_id, - 'price_tick': quote.price_tick, - 'volume_multiple': quote.volume_multiple, - 'margin_rate': quote.margin_rate, - 'expire_datetime': quote.expire_datetime, - 'create_datetime': quote.create_datetime - } - else: - # 返回模拟数据 + # 使用适配器的get_contract_info方法 + result = self.adapter.get_contract_info(symbol) + if result is None: + # 如果适配器返回None,使用模拟数据 return self._get_mock_contract_info(symbol) + return result except Exception as e: print(f"获取合约信息失败:{e}") return self._get_mock_contract_info(symbol) def get_market_data(self, symbols: List[str]) -> Dict[str, Dict]: """批量获取市场数据""" - market_data = {} - - for symbol in symbols: - try: - if TQSDK_AVAILABLE and self.api: - quote = self.api.get_quote(symbol) - self.api.wait_update() - - market_data[symbol] = { - 'latest_price': quote.last_price, - 'open': quote.open, - 'high': quote.high, - 'low': quote.low, - 'pre_close': quote.pre_close, - 'volume': quote.volume, - 'open_interest': quote.open_interest, - 'bid_price1': quote.bid_price1, - 'ask_price1': quote.ask_price1 - } - else: - # 模拟数据 + try: + # 使用适配器的get_market_data方法 + result = self.adapter.get_market_data(symbols) + if result: + return result + else: + # 如果适配器返回空,使用模拟数据 + market_data = {} + for symbol in symbols: market_data[symbol] = self._get_mock_market_data(symbol) - except Exception as e: - print(f"获取{symbol}市场数据失败:{e}") + return market_data + except Exception as e: + print(f"获取市场数据失败:{e}") + # 使用模拟数据 + market_data = {} + for symbol in symbols: market_data[symbol] = self._get_mock_market_data(symbol) - - return market_data + return market_data def _get_mock_kline_data(self, symbol: str, duration: str, count: int) -> pd.DataFrame: """获取模拟K线数据""" @@ -362,6 +177,8 @@ class DataFetcher: freq = '5T' elif duration == '15m': freq = '15T' + elif duration == '30m': + freq = '30T' elif duration == '1h': freq = '1H' elif duration == '1d': @@ -463,28 +280,108 @@ class DataFetcher: List[str]: 所有品种的合约代码列表 """ try: - if TQSDK_AVAILABLE and self.api: - # TQSDK 没有 get_instrument_info 方法,我们使用模拟数据 - print("TQSDK 不支持获取所有品种列表,使用模拟数据") - return self._get_mock_all_symbols() + # 使用适配器的get_all_symbols方法 + result = self.adapter.get_all_symbols() + if result: + return result else: - # 返回模拟数据 - print("使用模拟品种列表") - return self._get_mock_all_symbols() + # 如果适配器返回空,使用本地枚举数据 + print("使用本地枚举品种列表") + symbols_by_exchange = self.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 self._get_mock_all_symbols() def _get_mock_all_symbols(self) -> List[str]: """获取模拟品种列表""" - # 返回常用的期货品种 + # 返回用户指定的所有期货品种 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" + "AU2603", "AG2603", "CU2603", "NI2603", "SN2603", "FG2603", + "LY2603", "SA2603", "JM2603", "RB2603", "ALO2603", "MA2603", + "V2603", "FU2603", "SC2603", "AL2603", "P2603", "LI2603", + "SI2603", "RU2603", "BR2603", "ZN2603", "NR2603", "SP2603", + "IM2603", "IC2603", "LU2603", "IH2603" ] + + def get_all_symbols_by_exchange(self) -> Dict[str, Dict[str, List[str]]]: + """获取所有品种列表,按交易所-合约划分 + + Returns: + Dict[str, Dict[str, List[str]]]: 按交易所-合约划分的品种列表 + """ + # 本地枚举数据,按交易所-合约划分 + symbols_by_exchange = { + "SHFE": { # 上海期货交易所 + "AU": ["AU2603", "AU2604", "AU2605", "AU2606", "AU2607", "AU2608", "AU2609"], # 黄金 + "AG": ["AG2603", "AG2604", "AG2605", "AG2606", "AG2607", "AG2608", "AG2609"], # 白银 + "CU": ["CU2603", "CU2604", "CU2605", "CU2606", "CU2607", "CU2608", "CU2609"], # 铜 + "NI": ["NI2603", "NI2604", "NI2605", "NI2606", "NI2607", "NI2608", "NI2609"], # 镍 + "SN": ["SN2603", "SN2604", "SN2605", "SN2606", "SN2607", "SN2608", "SN2609"], # 锡 + "FG": ["FG2603", "FG2604", "FG2605", "FG2606", "FG2607", "FG2608", "FG2609"], # 玻璃 + "RB": ["RB2603", "RB2604", "RB2605", "RB2606", "RB2607", "RB2608", "RB2609"], # 螺纹钢 + "AL": ["AL2603", "AL2604", "AL2605", "AL2606", "AL2607", "AL2608", "AL2609"], # 铝 + "ZN": ["ZN2603", "ZN2604", "ZN2605", "ZN2606", "ZN2607", "ZN2608", "ZN2609"], # 锌 + "RU": ["RU2603", "RU2604", "RU2605", "RU2606", "RU2607", "RU2608", "RU2609"], # 橡胶 + "NR": ["NR2603", "NR2604", "NR2605", "NR2606", "NR2607", "NR2608", "NR2609"], # 20号胶 + "FU": ["FU2603", "FU2604", "FU2605", "FU2606", "FU2607", "FU2608", "FU2609"], # 燃油 + "SC": ["SC2603", "SC2604", "SC2605", "SC2606", "SC2607", "SC2608", "SC2609"], # 原油 + "LU": ["LU2603", "LU2604", "LU2605", "LU2606", "LU2607", "LU2608", "LU2609"], # 低硫燃油 + "ALO": ["ALO2603", "ALO2604", "ALO2605", "ALO2606", "ALO2607", "ALO2608", "ALO2609"], # 氧化铝 + "LI": ["LI2603", "LI2604", "LI2605", "LI2606", "LI2607", "LI2608", "LI2609"], # 碳酸锂 + "SI": ["SI2603", "SI2604", "SI2605", "SI2606", "SI2607", "SI2608", "SI2609"] # 工业硅 + }, + "INE": { # 上海国际能源交易中心 + "SC": ["SC2603", "SC2604", "SC2605", "SC2606", "SC2607", "SC2608", "SC2609"], # 原油 + "LU": ["LU2603", "LU2604", "LU2605", "LU2606", "LU2607", "LU2608", "LU2609"] # 低硫燃油 + }, + "DCE": { # 大连商品交易所 + "JM": ["JM2603", "JM2604", "JM2605", "JM2606", "JM2607", "JM2608", "JM2609"], # 焦煤 + "P": ["P2603", "P2604", "P2605", "P2606", "P2607", "P2608", "P2609"], # 棕榈油 + "V": ["V2603", "V2604", "V2605", "V2606", "V2607", "V2608", "V2609"], # PVC + "MA": ["MA2603", "MA2604", "MA2605", "MA2606", "MA2607", "MA2608", "MA2609"], # 甲醇 + "BR": ["BR2603", "BR2604", "BR2605", "BR2606", "BR2607", "BR2608", "BR2609"] # 合成橡胶 + }, + "CZCE": { # 郑州商品交易所 + "FG": ["FG2603", "FG2604", "FG2605", "FG2606", "FG2607", "FG2608", "FG2609"], # 玻璃 + "MA": ["MA2603", "MA2604", "MA2605", "MA2606", "MA2607", "MA2608", "MA2609"], # 甲醇 + "V": ["V2603", "V2604", "V2605", "V2606", "V2607", "V2608", "V2609"], # PVC + "SA": ["SA2603", "SA2604", "SA2605", "SA2606", "SA2607", "SA2608", "SA2609"], # 纯碱 + "LY": ["LY2603", "LY2604", "LY2605", "LY2606", "LY2607", "LY2608", "LY2609"] # 烧碱 + }, + "CFFEX": { # 中国金融期货交易所 + "IH": ["IH2603", "IH2604", "IH2605", "IH2606", "IH2607", "IH2608", "IH2609"], # 上证50 + "IC": ["IC2603", "IC2604", "IC2605", "IC2606", "IC2607", "IC2608", "IC2609"], # 中证500 + "IM": ["IM2603", "IM2604", "IM2605", "IM2606", "IM2607", "IM2608", "IM2609"] # 中证1000 + }, + "GEM": { # 广州期货交易所 + "SI": ["SI2603", "SI2604", "SI2605", "SI2606", "SI2607", "SI2608", "SI2609"], # 工业硅 + "SP": ["SP2603", "SP2604", "SP2605", "SP2606", "SP2607", "SP2608", "SP2609"] # 多晶硅 + } + } + + return symbols_by_exchange + + def get_contract_months(self, product_code: str) -> List[str]: + """获取合约的所有月份 + + Args: + product_code: 品种代码,如 "CU" + + Returns: + List[str]: 该品种的所有合约月份列表 + """ + # 本地枚举的合约月份 + contract_months = ["2603", "2604", "2605", "2606", "2607", "2608", "2609"] + + # 生成完整的合约代码 + return [f"{product_code}{month}" for month in contract_months] # 导入numpy diff --git a/requirements.txt b/requirements.txt index 7722634..57511cc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,8 +2,7 @@ numpy==1.26.4 pandas==2.2.1 matplotlib==3.8.3 scikit-learn==1.4.0 -tqsdk==1.6.3 -requests==2.31.0 +rqdatac==2.0.0 python-dotenv==1.0.0 APScheduler==3.10.4 pytest==7.4.4 diff --git a/test_config_switch.py b/test_config_switch.py new file mode 100644 index 0000000..353a96e --- /dev/null +++ b/test_config_switch.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +# 测试通过配置文件切换数据源 + +import os +import sys + +# 确保能导入项目模块 +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +# 从配置文件加载配置 +from config import ( + DATA_ADAPTER_TYPE, + TQSDK_USERNAME, + TQSDK_PASSWORD, + RQDATA_USERNAME, + RQDATA_PASSWORD, + DEBUG +) + +# 设置环境变量 +def setup_environment(): + """根据配置文件设置环境变量""" + print("=== 设置环境变量 ===") + + # 设置数据源类型 + os.environ["DATA_ADAPTER_TYPE"] = DATA_ADAPTER_TYPE + print(f"数据源类型: {DATA_ADAPTER_TYPE}") + + # 设置TQSDK账号 + if TQSDK_USERNAME: + os.environ["TQSDK_USERNAME"] = TQSDK_USERNAME + os.environ["TQSDK_PASSWORD"] = TQSDK_PASSWORD + print("TQSDK账号: 已配置") + else: + print("TQSDK账号: 未配置(将使用模拟数据)") + + # 设置RQData账号 + if RQDATA_USERNAME: + os.environ["RQDATA_USERNAME"] = RQDATA_USERNAME + os.environ["RQDATA_PASSWORD"] = RQDATA_PASSWORD + print("RQData账号: 已配置") + else: + print("RQData账号: 未配置(将使用模拟数据)") + + # 设置调试模式 + if DEBUG: + os.environ["DEBUG"] = "True" + print("调试模式: 开启") + else: + print("调试模式: 关闭") + + print("环境变量设置完成!\n") + +# 测试数据源连接 +def test_data_source(): + """测试数据源连接和基本功能""" + print("=== 测试数据源功能 ===") + + try: + from qihuo_analyzer.data.data_fetcher import DataFetcher + + # 创建数据获取器实例 + print("创建DataFetcher实例...") + fetcher = DataFetcher() + + # 查看使用的适配器 + adapter_name = fetcher.adapter.__class__.__name__ + print(f"当前使用的适配器: {adapter_name}") + + # 测试获取品种列表 + print("\n测试获取品种列表...") + symbols = fetcher.get_all_symbols() + if symbols: + print(f"成功获取 {len(symbols)} 个品种") + print("前10个品种:", symbols[:10]) + else: + print("获取品种列表失败") + + # 测试获取K线数据 + print("\n测试获取K线数据...") + test_symbol = symbols[0] if symbols else "CU2603" + kline_data = fetcher.get_kline_data( + symbol=test_symbol, + duration="1h", # 1小时 + count=50 + ) + + if kline_data is not None: + print(f"成功获取 {len(kline_data)} 条K线数据") + print("数据示例:") + print(kline_data.head()) + else: + print("获取K线数据失败") + + print("\n=== 测试完成 ===") + return True + + except Exception as e: + print(f"测试失败: {e}") + import traceback + traceback.print_exc() + return False + +# 主函数 +def main(): + """主测试函数""" + print("开始测试通过配置文件切换数据源...\n") + + # 1. 设置环境变量 + setup_environment() + + # 2. 测试数据源 + success = test_data_source() + + if success: + print("\n✓ 数据源切换测试成功!") + print(f"✓ 当前使用的数据源类型: {DATA_ADAPTER_TYPE}") + else: + print("\n✗ 数据源切换测试失败!") + +if __name__ == "__main__": + main() diff --git a/test_data_adapters.py b/test_data_adapters.py new file mode 100644 index 0000000..2754716 --- /dev/null +++ b/test_data_adapters.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +# 测试数据适配器 + +from qihuo_analyzer.data.api_adapters import DataAdapterFactory + + +def test_adapter(adapter_type): + """测试数据适配器 + + Args: + adapter_type: 适配器类型,可选值:'tqsdk', 'rqdata' + """ + print(f"\n=== 测试 {adapter_type} 适配器 ===") + + # 创建适配器 + adapter = DataAdapterFactory.create_adapter(adapter_type) + + # 连接API + print("连接API...") + connected = adapter.connect() + print(f"连接结果: {'成功' if connected else '失败'}") + + if connected: + # 测试获取K线数据 + print("\n测试获取K线数据...") + kline_data = adapter.get_kline_data('CU2603', '1d', 10) + if kline_data is not None: + print(f"成功获取K线数据,数据长度: {len(kline_data)}") + print(kline_data.head()) + else: + print("获取K线数据失败") + + # 测试获取Tick数据 + print("\n测试获取Tick数据...") + tick_data = adapter.get_tick_data('CU2603', 100) + if tick_data is not None: + print(f"成功获取Tick数据,数据长度: {len(tick_data)}") + print(tick_data.head()) + else: + print("获取Tick数据失败") + + # 测试获取合约信息 + print("\n测试获取合约信息...") + contract_info = adapter.get_contract_info('CU2603') + if contract_info is not None: + print("成功获取合约信息:") + for key, value in contract_info.items(): + print(f"{key}: {value}") + else: + print("获取合约信息失败") + + # 测试批量获取市场数据 + print("\n测试批量获取市场数据...") + market_data = adapter.get_market_data(['CU2603', 'AL2603']) + if market_data: + print("成功获取市场数据:") + for symbol, data in market_data.items(): + print(f"{symbol}: {data}") + else: + print("获取市场数据失败") + + # 测试获取所有品种列表 + print("\n测试获取所有品种列表...") + symbols = adapter.get_all_symbols() + if symbols: + print(f"成功获取品种列表,共{len(symbols)}个品种") + print(f"前10个品种: {symbols[:10]}") + else: + print("获取品种列表失败") + + # 断开连接 + print("\n断开连接...") + adapter.disconnect() + else: + print("API连接失败,跳过测试") + + +if __name__ == "__main__": + # 测试TQSDK适配器 + test_adapter('tqsdk') + + # 测试RQData适配器 + test_adapter('rqdata') + + print("\n=== 测试完成 ===") diff --git a/test_enum_symbols.py b/test_enum_symbols.py new file mode 100644 index 0000000..dcfb3ad --- /dev/null +++ b/test_enum_symbols.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +# 测试枚举合约数据 + +import os +import sys + +# 确保能导入项目模块 +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from qihuo_analyzer.data.data_fetcher import DataFetcher + +def test_enum_symbols(): + """测试枚举合约数据""" + print("=== 测试枚举合约数据 ===") + + # 创建DataFetcher实例 + fetcher = DataFetcher() + + # 测试获取所有品种列表 + print("\n1. 测试获取所有品种列表:") + symbols = fetcher.get_all_symbols() + print(f"成功获取 {len(symbols)} 个品种") + print("所有品种:", symbols) + + # 测试获取按交易所划分的品种列表 + print("\n2. 测试获取按交易所划分的品种列表:") + symbols_by_exchange = fetcher.get_all_symbols_by_exchange() + for exchange, products in symbols_by_exchange.items(): + print(f"\n交易所: {exchange}") + print(f"品种数量: {len(products)}") + for product, contracts in products.items(): + print(f" {product}: {contracts[:3]}... (共{len(contracts)}个合约)") + + # 测试用户指定的品种是否都存在 + print("\n3. 测试用户指定的品种是否都存在:") + specified_products = [ + "金", "银", "铜", "镍", "锡", "玻璃", "烧碱", "纯碱", "焦煤", + "螺纹钢", "氧化铝", "甲醇", "PVC", "燃油", "原油", "铝", "棕榈油", + "碳酸锂", "工业硅", "橡胶", "合成橡胶", "锌", "20号胶", "多晶硅", + "中证1000", "中证500", "低硫燃油", "上证50" + ] + + # 品种中文名称映射 + product_name_map = { + 'AU': '金', + 'AG': '银', + 'CU': '铜', + 'NI': '镍', + 'SN': '锡', + 'FG': '玻璃', + 'LY': '烧碱', + 'SA': '纯碱', + 'JM': '焦煤', + 'RB': '螺纹钢', + 'ALO': '氧化铝', + 'MA': '甲醇', + 'V': 'PVC', + 'FU': '燃油', + 'SC': '原油', + 'AL': '铝', + 'P': '棕榈油', + 'LI': '碳酸锂', + 'SI': '工业硅', + 'RU': '橡胶', + 'BR': '合成橡胶', + 'ZN': '锌', + 'NR': '20号胶', + 'SP': '多晶硅', + 'IM': '中证1000', + 'IC': '中证500', + 'LU': '低硫燃油', + 'IH': '上证50' + } + + # 检查每个指定的品种 + found_products = [] + missing_products = [] + + for product_code, product_name in product_name_map.items(): + # 检查是否在模拟品种列表中 + mock_symbols = fetcher._get_mock_all_symbols() + product_found = any(symbol.startswith(product_code) for symbol in mock_symbols) + + # 检查是否在按交易所划分的列表中 + exchange_found = False + for exchange, products in symbols_by_exchange.items(): + if product_code in products: + exchange_found = True + break + + if product_found or exchange_found: + found_products.append(product_name) + print(f"✓ 找到: {product_name} ({product_code})") + else: + missing_products.append(product_name) + print(f"✗ 缺失: {product_name} ({product_code})") + + print(f"\n4. 检查结果:") + print(f"找到的品种: {len(found_products)}/{len(specified_products)}") + print(f"缺失的品种: {len(missing_products)}/{len(specified_products)}") + + if missing_products: + print(f"缺失的品种: {missing_products}") + else: + print("✓ 所有用户指定的品种都已找到!") + + # 测试获取K线数据 + print("\n5. 测试获取K线数据:") + test_symbols = ['AU2603', 'AG2603', 'CU2603', 'NI2603', 'SI2603'] + for symbol in test_symbols: + try: + data = fetcher.get_kline_data(symbol, "1h", 10) + if data is not None: + print(f"✓ 成功获取 {symbol} 的K线数据 ({len(data)}条)") + else: + print(f"✗ 无法获取 {symbol} 的K线数据") + except Exception as e: + print(f"✗ 获取 {symbol} 数据失败: {e}") + + print("\n=== 测试完成 ===") + +if __name__ == "__main__": + test_enum_symbols() diff --git a/test_iron_ore_fix.py b/test_iron_ore_fix.py new file mode 100644 index 0000000..2bc30b7 --- /dev/null +++ b/test_iron_ore_fix.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# 测试铁矿石合约代码修复 + +import os +import sys + +# 确保能导入项目模块 +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from qihuo_analyzer.data.api_adapters.tqsdk_adapter import TqSdkAdapter +from qihuo_analyzer.data.data_fetcher import DataFetcher + +def test_iron_ore_contract(): + """测试铁矿石合约代码处理""" + print("=== 测试铁矿石合约代码修复 ===") + + # 测试TQSDK适配器的合约代码转换 + print("\n1. 测试TQSDK适配器合约代码转换:") + adapter = TqSdkAdapter() + + test_symbols = [ + "I2603", # 铁矿石 + "J2603", # 焦炭 + "JM2603", # 焦煤 + "CU2603", # 铜 + "AL2603", # 铝 + "AU2603", # 黄金 + "AG2603", # 白银 + "RB2603", # 螺纹钢 + "P2603", # 棕榈油 + "V2603", # PVC + "MA2603", # 甲醇 + "FG2603", # 玻璃 + "SA2603", # 纯碱 + "LY2603", # 烧碱 + "LI2603", # 碳酸锂 + "SI2603", # 工业硅 + "SP2603", # 多晶硅 + "RU2603", # 橡胶 + "NR2603", # 20号胶 + "FU2603", # 燃油 + "SC2603", # 原油 + "LU2603", # 低硫燃油 + "ALO2603",# 氧化铝 + "BR2603", # 合成橡胶 + ] + + for symbol in test_symbols: + try: + tq_symbol = adapter._convert_symbol(symbol) + print(f"✓ {symbol} → {tq_symbol}") + except Exception as e: + print(f"✗ {symbol} → 转换失败: {e}") + + # 测试DataFetcher获取铁矿石数据 + print("\n2. 测试DataFetcher获取铁矿石数据:") + fetcher = DataFetcher() + + try: + # 测试获取铁矿石K线数据 + print("测试获取 I2603 的K线数据...") + kline_data = fetcher.get_kline_data("I2603", "1h", 10) + if kline_data is not None: + print(f"✓ 成功获取 {len(kline_data)} 条铁矿石K线数据") + print("数据示例:") + print(kline_data.head()) + else: + print("✗ 无法获取铁矿石K线数据") + except Exception as e: + print(f"✗ 获取铁矿石数据失败: {e}") + + # 测试其他单字符品种 + print("\n3. 测试其他单字符品种:") + single_char_symbols = ["I2603", "J2603", "P2603", "C2603", "L2603"] + + for symbol in single_char_symbols: + try: + print(f"测试获取 {symbol} 的K线数据...") + kline_data = fetcher.get_kline_data(symbol, "1h", 5) + if kline_data is not None: + print(f"✓ 成功获取 {symbol} 的K线数据") + else: + print(f"✗ 无法获取 {symbol} 的K线数据") + except Exception as e: + print(f"✗ 获取 {symbol} 数据失败: {e}") + + print("\n=== 测试完成 ===") + +if __name__ == "__main__": + test_iron_ore_contract() diff --git a/test_mock_symbols.py b/test_mock_symbols.py new file mode 100644 index 0000000..c544267 --- /dev/null +++ b/test_mock_symbols.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +# 测试TQSDK适配器的_get_mock_all_symbols方法 + +import os +import sys + +# 确保能导入项目模块 +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from qihuo_analyzer.data.api_adapters.tqsdk_adapter import TqSdkAdapter + +def test_mock_all_symbols(): + """测试_get_mock_all_symbols方法是否返回exchange_map中映射的所有品种""" + print("=== 测试TQSDK适配器_get_mock_all_symbols方法 ===") + + # 创建TQSDK适配器实例 + adapter = TqSdkAdapter() + + # 获取模拟品种列表 + mock_symbols = adapter._get_mock_all_symbols() + + print(f"\n1. 测试结果:") + print(f"模拟品种列表长度: {len(mock_symbols)}") + print(f"exchange_map长度: {len(adapter.exchange_map)}") + print(f"两者长度是否一致: {len(mock_symbols) == len(adapter.exchange_map)}") + + print("\n2. 模拟品种列表:") + print(sorted(mock_symbols)) + + print("\n3. exchange_map中的品种:") + print(sorted(adapter.exchange_map.keys())) + + # 验证所有exchange_map中的品种都在模拟列表中 + print("\n4. 验证所有exchange_map中的品种都在模拟列表中:") + missing_symbols = [] + for product_code in adapter.exchange_map: + expected_symbol = f"{product_code}2603" + if expected_symbol not in mock_symbols: + missing_symbols.append(expected_symbol) + + if missing_symbols: + print(f"✗ 缺失的品种: {missing_symbols}") + else: + print("✓ 所有exchange_map中的品种都在模拟列表中") + + # 验证模拟列表中的品种都在exchange_map中 + print("\n5. 验证模拟列表中的品种都在exchange_map中:") + invalid_symbols = [] + for symbol in mock_symbols: + # 提取品种代码 + if len(symbol) == 5: + # 3字符品种代码 + product_code = symbol[:3] + elif len(symbol) == 4: + # 2字符或1字符品种代码 + # 先尝试2字符 + product_code = symbol[:2] + if product_code not in adapter.exchange_map: + # 尝试1字符 + product_code = symbol[:1] + else: + product_code = symbol[:2] + + if product_code not in adapter.exchange_map: + invalid_symbols.append(symbol) + + if invalid_symbols: + print(f"✗ 无效的品种: {invalid_symbols}") + else: + print("✓ 模拟列表中的所有品种都在exchange_map中") + + # 测试get_all_symbols方法 + print("\n6. 测试get_all_symbols方法:") + all_symbols = adapter.get_all_symbols() + print(f"get_all_symbols返回长度: {len(all_symbols)}") + print(f"与模拟列表长度是否一致: {len(all_symbols) == len(mock_symbols)}") + + print("\n=== 测试完成 ===") + +if __name__ == "__main__": + test_mock_all_symbols() diff --git a/test_tq_symbol_fix.py b/test_tq_symbol_fix.py new file mode 100644 index 0000000..e5b72a5 --- /dev/null +++ b/test_tq_symbol_fix.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +# 测试TQSDK合约代码格式修复 + +import os +import sys + +# 确保能导入项目模块 +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from qihuo_analyzer.data.api_adapters.tqsdk_adapter import TqSdkAdapter +from qihuo_analyzer.data.data_fetcher import DataFetcher + +def test_tq_symbol_format(): + """测试TQSDK合约代码格式""" + print("=== 测试TQSDK合约代码格式修复 ===") + + # 测试TQSDK适配器的合约代码转换 + print("\n1. 测试TQSDK适配器合约代码转换:") + adapter = TqSdkAdapter() + + test_symbols = [ + "TA2603", # PTA + "CF2603", # 棉花 + "SR2603", # 白糖 + "MA2603", # 甲醇 + "ZC2603", # 动力煤 + "FG2603", # 玻璃 + "RM2603", # 菜籽粕 + "OI2603", # 菜籽油 + "SA2603", # 纯碱 + "LY2603", # 烧碱 + "CU2603", # 铜 + "AL2603", # 铝 + "ZN2603", # 锌 + "PB2603", # 铅 + "NI2603", # 镍 + "SN2603", # 锡 + "AU2603", # 黄金 + "AG2603", # 白银 + "RB2603", # 螺纹钢 + "HC2603", # 热轧卷板 + "BU2603", # 沥青 + "RU2603", # 橡胶 + "FU2603", # 燃油 + "I2603", # 铁矿石 + "J2603", # 焦炭 + "JM2603", # 焦煤 + "A2603", # 大豆 + "B2603", # 豆粕 + "M2603", # 豆粕 + "Y2603", # 豆油 + "P2603", # 棕榈油 + "C2603", # 玉米 + "CS2603", # 玉米淀粉 + "L2603", # 聚乙烯 + "V2603", # 聚氯乙烯 + "PP2603", # 聚丙烯 + ] + + for symbol in test_symbols: + try: + tq_symbol = adapter._convert_symbol(symbol) + print(f"✓ {symbol} → {tq_symbol}") + except Exception as e: + print(f"✗ {symbol} → 转换失败: {e}") + + # 测试DataFetcher获取PTA数据 + print("\n2. 测试DataFetcher获取PTA数据:") + fetcher = DataFetcher() + + try: + # 测试获取PTA K线数据 + print("测试获取 TA2603 的K线数据...") + kline_data = fetcher.get_kline_data("TA2603", "1h", 10) + if kline_data is not None: + print(f"✓ 成功获取 {len(kline_data)} 条PTA K线数据") + print("数据示例:") + print(kline_data.head()) + else: + print("✗ 无法获取PTA K线数据") + except Exception as e: + print(f"✗ 获取PTA数据失败: {e}") + + # 测试获取其他郑州商品交易所数据 + print("\n3. 测试获取其他郑州商品交易所数据:") + czce_symbols = ["CF2603", "SR2603", "MA2603", "FG2603"] + + for symbol in czce_symbols: + try: + print(f"测试获取 {symbol} 的K线数据...") + kline_data = fetcher.get_kline_data(symbol, "1h", 5) + if kline_data is not None: + print(f"✓ 成功获取 {symbol} 的K线数据") + else: + print(f"✗ 无法获取 {symbol} 的K线数据") + except Exception as e: + print(f"✗ 获取 {symbol} 数据失败: {e}") + + print("\n=== 测试完成 ===") + +if __name__ == "__main__": + test_tq_symbol_format() diff --git a/web/__pycache__/auth.cpython-311.pyc b/web/__pycache__/auth.cpython-311.pyc new file mode 100644 index 0000000..8dccc97 Binary files /dev/null and b/web/__pycache__/auth.cpython-311.pyc differ diff --git a/品种备份.ini b/品种备份.ini new file mode 100644 index 0000000..e692eed --- /dev/null +++ b/品种备份.ini @@ -0,0 +1,49 @@ +self.exchange_map = { + 'AU': 'SHFE', # 黄金 - 上海期货交易所 + 'AG': 'SHFE', # 白银 - 上海期货交易所 + 'CU': 'SHFE', # 铜 - 上海期货交易所 + 'NI': 'SHFE', # 镍 - 上海期货交易所 + 'SN': 'SHFE', # 锡 - 上海期货交易所 + 'FG': 'CZCE', # 玻璃 - 郑州商品交易所 + 'LY': 'CZCE', # 烧碱 - 郑州商品交易所 + 'SA': 'CZCE', # 纯碱 - 郑州商品交易所 + 'JM': 'DCE', # 焦煤 - 大连商品交易所 + 'RB': 'SHFE', # 螺纹钢 - 上海期货交易所 + 'ALO': 'SHFE', # 氧化铝 - 上海期货交易所 + 'MA': 'CZCE', # 甲醇 - 郑州商品交易所 + 'V': 'DCE', # PVC - 大连商品交易所 + 'FU': 'SHFE', # 燃油 - 上海期货交易所 + 'SC': 'INE', # 原油 - 上海国际能源交易中心 + 'AL': 'SHFE', # 铝 - 上海期货交易所 + 'P': 'DCE', # 棕榈油 - 大连商品交易所 + 'LI': 'SHFE', # 碳酸锂 - 上海期货交易所 + 'SI': 'GEM', # 工业硅 - 广州期货交易所 + 'RU': 'SHFE', # 橡胶 - 上海期货交易所 + 'BR': 'DCE', # 合成橡胶 - 大连商品交易所 + 'ZN': 'SHFE', # 锌 - 上海期货交易所 + 'NR': 'SHFE', # 20号胶 - 上海期货交易所 + 'SP': 'GEM', # 多晶硅 - 广州期货交易所 + 'IM': 'CFFEX', # 中证1000 - 中国金融期货交易所 + 'IC': 'CFFEX', # 中证500 - 中国金融期货交易所 + 'LU': 'INE', # 低硫燃油 - 上海国际能源交易中心 + 'IH': 'CFFEX', # 上证50 - 中国金融期货交易所 + 'HC': 'SHFE', # 热轧卷板 - 上海期货交易所 + 'BU': 'SHFE', # 沥青 - 上海期货交易所 + 'PB': 'SHFE', # 铅 - 上海期货交易所 + 'I': 'DCE', # 铁矿石 - 大连商品交易所 + 'J': 'DCE', # 焦炭 - 大连商品交易所 + 'A': 'DCE', # 大豆 - 大连商品交易所 + 'B': 'DCE', # 豆粕 - 大连商品交易所 + 'M': 'DCE', # 豆粕 - 大连商品交易所 + 'Y': 'DCE', # 豆油 - 大连商品交易所 + 'C': 'DCE', # 玉米 - 大连商品交易所 + 'CS': 'DCE', # 玉米淀粉 - 大连商品交易所 + 'L': 'DCE', # 聚乙烯 - 大连商品交易所 + 'PP': 'DCE', # 聚丙烯 - 大连商品交易所 + 'TA': 'CZCE', # PTA - 郑州商品交易所 + 'CF': 'CZCE', # 棉花 - 郑州商品交易所 + 'SR': 'CZCE', # 白糖 - 郑州商品交易所 + 'ZC': 'CZCE', # 动力煤 - 郑州商品交易所 + 'RM': 'CZCE', # 菜籽粕 - 郑州商品交易所 + 'OI': 'CZCE', # 菜籽油 - 郑州商品交易所 + } \ No newline at end of file diff --git a/数据源切换指南.md b/数据源切换指南.md new file mode 100644 index 0000000..a4dedac --- /dev/null +++ b/数据源切换指南.md @@ -0,0 +1,295 @@ +# 数据源切换指南 + +本指南详细说明了如何在AlphaFutures系统中切换不同的数据源(TQSDK、RQData等)。 + +## 方法一:通过环境变量配置(推荐) + +### 1. Windows系统环境变量配置 + +1. **打开系统属性**: + - 右键点击"此电脑" → 选择"属性" + - 点击"高级系统设置" + +2. **进入环境变量设置**: + - 点击"环境变量"按钮 + +3. **添加/修改系统变量**: + - 在"系统变量"区域点击"新建"(如果已存在则点击"编辑") + - 变量名:`DATA_ADAPTER_TYPE` + - 变量值: + - `tqsdk` - 使用TQSDK数据源 + - `rqdata` - 使用RQData数据源 + +4. **设置账号信息**(可选): + - **TQSDK账号**: + - 新建变量 `TQSDK_USERNAME`,值为你的TQSDK用户名 + - 新建变量 `TQSDK_PASSWORD`,值为你的TQSDK密码 + - **RQData账号**: + - 新建变量 `RQDATA_USERNAME`,值为你的RQData用户名 + - 新建变量 `RQDATA_PASSWORD`,值为你的RQData密码 + +5. **生效配置**: + - 点击"确定"保存所有设置 + - **重启应用程序**:环境变量变更需要重启应用才能生效 + +### 2. Linux/Mac系统环境变量配置 + +1. **编辑配置文件**: + ```bash + # 对于bash用户 + nano ~/.bashrc + + # 对于zsh用户 + nano ~/.zshrc + ``` + +2. **添加环境变量**: + ```bash + # 设置数据源类型 + export DATA_ADAPTER_TYPE="rqdata" + + # 设置RQData账号(可选) + export RQDATA_USERNAME="your_username" + export RQDATA_PASSWORD="your_password" + ``` + +3. **生效配置**: + ```bash + # 对于bash用户 + source ~/.bashrc + + # 对于zsh用户 + source ~/.zshrc + ``` + +## 方法二:在代码中临时切换 + +### 1. 临时设置环境变量 + +```python +import os + +# 临时切换到RQData数据源 +os.environ["DATA_ADAPTER_TYPE"] = "rqdata" + +# 设置RQData账号(可选) +os.environ["RQDATA_USERNAME"] = "your_username" +os.environ["RQDATA_PASSWORD"] = "your_password" + +# 然后创建DataFetcher实例 +from qihuo_analyzer.data.data_fetcher import DataFetcher +fetcher = DataFetcher() + +# 现在fetcher会使用RQData数据源 +kline_data = fetcher.get_kline_data("SHFE.rb2409", 3600, start_time, end_time) +``` + +### 2. 直接指定适配器 + +```python +from qihuo_analyzer.data.api_adapters.adapter_factory import DataAdapterFactory + +# 直接创建RQData适配器 +rqdata_adapter = DataAdapterFactory.create_adapter("rqdata") +rqdata_adapter.connect() + +# 使用RQData获取数据 +data = rqdata_adapter.get_kline_data("SHFE.rb2409", 3600, start_time, end_time) + +# 关闭连接 +rqdata_adapter.disconnect() + +# 切换到TQSDK适配器 +tqsdk_adapter = DataAdapterFactory.create_adapter("tqsdk") +tqsdk_adapter.connect() + +# 使用TQSDK获取数据 +data = tqsdk_adapter.get_kline_data("SHFE.rb2409", 3600, start_time, end_time) + +# 关闭连接 +tqsdk_adapter.disconnect() +``` + +## 方法三:在配置文件中设置 + +如果系统支持配置文件,可以在配置文件中添加数据源设置: + +### 1. 创建配置文件 + +在项目根目录创建 `config.py` 文件: + +```python +# config.py +DATA_ADAPTER_TYPE = "rqdata" # 可选值: tqsdk, rqdata + +# RQData账号配置(可选) +RQDATA_USERNAME = "your_username" +RQDATA_PASSWORD = "your_password" + +# TQSDK账号配置(可选) +TQSDK_USERNAME = "your_username" +TQSDK_PASSWORD = "your_password" +``` + +### 2. 在代码中加载配置 + +```python +import os +from config import DATA_ADAPTER_TYPE, RQDATA_USERNAME, RQDATA_PASSWORD, TQSDK_USERNAME, TQSDK_PASSWORD + +# 设置环境变量 +os.environ["DATA_ADAPTER_TYPE"] = DATA_ADAPTER_TYPE + +# 设置账号信息 +if RQDATA_USERNAME: + os.environ["RQDATA_USERNAME"] = RQDATA_USERNAME +if RQDATA_PASSWORD: + os.environ["RQDATA_PASSWORD"] = RQDATA_PASSWORD +if TQSDK_USERNAME: + os.environ["TQSDK_USERNAME"] = TQSDK_USERNAME +if TQSDK_PASSWORD: + os.environ["TQSDK_PASSWORD"] = TQSDK_PASSWORD + +# 创建DataFetcher实例 +from qihuo_analyzer.data.data_fetcher import DataFetcher +fetcher = DataFetcher() +``` + +## 查看当前使用的数据源 + +### 1. 代码中查看 + +```python +from qihuo_analyzer.data.data_fetcher import DataFetcher + +# 创建DataFetcher实例 +fetcher = DataFetcher() + +# 查看当前使用的适配器类型 +print(f"当前使用的数据源: {fetcher.adapter.__class__.__name__}") + +# 查看适配器配置信息 +print(f"适配器配置: {fetcher.adapter.config}") +``` + +### 2. 检查环境变量 + +```python +import os + +# 查看当前数据源类型 +adapter_type = os.environ.get("DATA_ADAPTER_TYPE", "tqsdk") +print(f"当前配置的数据源类型: {adapter_type}") + +# 查看账号配置 +if adapter_type == "rqdata": + username = os.environ.get("RQDATA_USERNAME", "未配置") + print(f"RQData账号: {username}") +elif adapter_type == "tqsdk": + username = os.environ.get("TQSDK_USERNAME", "未配置") + print(f"TQSDK账号: {username}") +``` + +## 切换数据源的注意事项 + +### 1. 账号配置 + +- **切换到TQSDK**:需要配置TQSDK账号密码,否则使用模拟数据 +- **切换到RQData**:需要配置RQData账号密码,否则使用模拟数据 +- **未配置账号**:系统会自动使用模拟数据,确保系统正常运行 + +### 2. 数据一致性 + +- 不同数据源的数据格式可能略有差异,但统一接口会确保返回格式一致 +- 历史数据的时间范围和精度可能因数据源而异 +- 实时数据的更新频率可能不同 + +### 3. 性能考虑 + +- **TQSDK**:适合实时数据和高频交易策略 +- **RQData**:适合历史数据回测和研究分析 +- 选择适合你使用场景的数据源 + +### 4. 错误处理 + +- 切换数据源后,建议测试基本功能确保连接正常 +- 如遇连接问题,检查账号配置和网络连接 +- 系统会自动处理API错误并使用模拟数据作为降级方案 + +## 常见问题解决 + +### Q: 切换数据源后连接失败 + +**A:** +1. 检查账号密码是否正确 +2. 检查网络连接是否正常 +3. 检查API服务是否正常运行 +4. 尝试重启应用程序 + +### Q: 如何在运行时动态切换数据源 + +**A:** +```python +import os +from qihuo_analyzer.data.data_fetcher import DataFetcher + +# 先使用TQSDK +os.environ["DATA_ADAPTER_TYPE"] = "tqsdk" +fetcher1 = DataFetcher() + +# 再使用RQData +os.environ["DATA_ADAPTER_TYPE"] = "rqdata" +fetcher2 = DataFetcher() + +# 现在fetcher1使用TQSDK,fetcher2使用RQData +``` + +### Q: 数据源切换后数据格式不一致 + +**A:** +统一接口已处理数据格式问题,确保返回格式一致。如果遇到问题,请检查: +1. 确认使用的是最新版本的统一接口 +2. 检查数据获取参数是否正确 +3. 查看日志中的错误信息 + +## 示例:完整的数据源切换流程 + +### 场景:从TQSDK切换到RQData + +1. **设置环境变量**: + - `DATA_ADAPTER_TYPE` = "rqdata" + - `RQDATA_USERNAME` = "your_rqdata_username" + - `RQDATA_PASSWORD` = "your_rqdata_password" + +2. **重启应用**:确保环境变量生效 + +3. **验证切换**: + ```python + from qihuo_analyzer.data.data_fetcher import DataFetcher + + # 创建DataFetcher实例 + fetcher = DataFetcher() + + # 测试数据获取 + try: + # 获取K线数据 + data = fetcher.get_kline_data( + symbol="SHFE.rb2409", + interval=3600, + start_time="2024-01-01 00:00:00", + end_time="2024-01-02 00:00:00" + ) + print("数据获取成功!") + print(f"获取到 {len(data)} 条数据") + print(f"当前使用的数据源: {fetcher.adapter.__class__.__name__}") + except Exception as e: + print(f"数据获取失败: {e}") + ``` + +4. **确认切换成功**: + - 查看输出中的"当前使用的数据源"信息 + - 检查数据获取是否正常 + +## 总结 + +通过以上方法,你可以轻松在不同的数据源之间切换,选择最适合你使用场景的数据源。统一接口的设计确保了切换数据源不会影响业务逻辑,为系统提供了更大的灵活性。 \ No newline at end of file diff --git a/统一数据获取接口开发文档.md b/统一数据获取接口开发文档.md new file mode 100644 index 0000000..7282e11 --- /dev/null +++ b/统一数据获取接口开发文档.md @@ -0,0 +1,354 @@ +# 统一数据获取接口开发文档 + +## 1. 概述 + +本开发文档描述了AlphaFutures系统中统一数据获取接口的设计与实现。该接口采用适配器设计模式,支持多种数据源(TQSDK、RQData等),为系统提供统一的数据获取出口,实现业务逻辑与数据获取的解耦。 + +### 1.1 设计目标 + +- **统一接口**:为所有数据源提供一致的API接口 +- **可扩展性**:支持轻松添加新的数据源适配器 +- **配置灵活**:通过配置选择不同的数据源 +- **容错处理**:包含错误处理和降级机制 +- **模拟数据**:支持无账号时的模拟数据功能 + +### 1.2 技术栈 + +- Python 3.8+ +- 抽象基类(ABC) +- 工厂模式 +- 依赖注入 +- 环境变量配置 + +## 2. 架构设计 + +### 2.1 整体架构 + +``` +业务逻辑层 + ↑ + | 依赖注入 + ↓ +统一数据获取层 (DataFetcher) + ↑ + | 工厂模式 + ↓ +适配器抽象层 (BaseDataAdapter) + ↑ + | 继承实现 + ↓ +具体适配器实现 +├── TQSdkAdapter +├── RQDataAdapter +└── 其他适配器... +``` + +### 2.2 核心设计模式 + +1. **适配器模式**:将不同数据源的接口转换为统一接口 +2. **工厂模式**:根据配置创建对应的适配器实例 +3. **依赖注入**:业务逻辑通过统一接口获取数据,不直接依赖具体实现 + +## 3. 核心组件 + +### 3.1 抽象基类 (BaseDataAdapter) + +定义了所有数据适配器必须实现的接口方法,确保一致性。 + +**文件路径**:`qihuo_analyzer/data/api_adapters/base_adapter.py` + +**核心方法**: +- `connect()` - 连接数据源 +- `disconnect()` - 断开连接 +- `get_kline_data()` - 获取K线数据 +- `get_tick_data()` - 获取tick数据 +- `get_contract_info()` - 获取合约信息 +- `get_market_data()` - 获取市场数据 +- `get_all_symbols()` - 获取所有品种列表 + +### 3.2 具体适配器 + +#### 3.2.1 TQSDK适配器 + +**文件路径**:`qihuo_analyzer/data/api_adapters/tqsdk_adapter.py` + +实现了TQSDK的所有接口方法,包含错误处理和模拟数据支持。 + +#### 3.2.2 RQData适配器 + +**文件路径**:`qihuo_analyzer/data/api_adapters/rqdata_adapter.py` + +实现了RQData的所有接口方法,包含错误处理和模拟数据支持。 + +### 3.3 适配器工厂 + +**文件路径**:`qihuo_analyzer/data/api_adapters/adapter_factory.py` + +根据配置创建对应的适配器实例,支持通过环境变量选择数据源类型。 + +### 3.4 统一数据获取器 + +**文件路径**:`qihuo_analyzer/data/data_fetcher.py` + +使用统一接口获取数据,替代直接API调用,为业务逻辑提供一致的数据获取方式。 + +## 4. 实现细节 + +### 4.1 配置管理 + +#### 4.1.1 环境变量配置 + +| 环境变量 | 说明 | 默认值 | +|---------|------|--------| +| DATA_ADAPTER_TYPE | 数据适配器类型 (tqsdk/rqdata) | tqsdk | +| TQSDK_USERNAME | TQSDK账号 | 无 | +| TQSDK_PASSWORD | TQSDK密码 | 无 | +| RQDATA_USERNAME | RQData账号 | 无 | +| RQDATA_PASSWORD | RQData密码 | 无 | + +#### 4.1.2 账号配置 + +- **TQSDK**:设置`TQSDK_USERNAME`和`TQSDK_PASSWORD`环境变量 +- **RQData**:设置`RQDATA_USERNAME`和`RQDATA_PASSWORD`环境变量 +- **未配置账号**:自动使用模拟数据 + +### 4.2 错误处理 + +所有适配器都包含完整的错误处理机制: + +1. **连接错误**:捕获连接异常,返回错误信息并使用模拟数据 +2. **数据获取错误**:捕获API调用异常,返回错误信息并使用模拟数据 +3. **参数错误**:验证输入参数,返回错误信息 + +### 4.3 模拟数据 + +当API连接失败或未配置账号时,自动使用模拟数据: + +- **K线数据**:生成随机波动的价格数据 +- **Tick数据**:生成随机的成交数据 +- **合约信息**:返回基本合约信息 +- **市场数据**:返回模拟的市场行情 +- **品种列表**:返回内置的品种列表 + +### 4.4 数据格式统一 + +所有适配器返回的数据格式保持一致: + +- **K线数据**:包含时间、开盘价、最高价、最低价、收盘价、成交量 +- **Tick数据**:包含时间、最新价、成交量、持仓量、买价、买量、卖价、卖量 +- **合约信息**:包含合约代码、交易所、品种类型、合约大小等 +- **市场数据**:包含最新价、涨跌幅、成交量、持仓量等 +- **品种列表**:包含品种代码、交易所、品种名称等 + +## 5. 使用方法 + +### 5.1 基本使用 + +```python +from qihuo_analyzer.data.data_fetcher import DataFetcher + +# 创建数据获取器实例(自动使用配置的适配器) +fetcher = DataFetcher() + +# 获取K线数据 +kline_data = fetcher.get_kline_data( + symbol="SHFE.rb2409", + interval=3600, # 1小时 + start_time="2024-01-01 00:00:00", + end_time="2024-01-02 00:00:00" +) + +# 获取Tick数据 +tick_data = fetcher.get_tick_data( + symbol="SHFE.rb2409", + start_time="2024-01-01 09:00:00", + end_time="2024-01-01 11:30:00" +) + +# 获取合约信息 +contract_info = fetcher.get_contract_info("SHFE.rb2409") + +# 获取市场数据 +market_data = fetcher.get_market_data(["SHFE.rb2409", "DCE.i2409"]) + +# 获取所有品种列表 +symbols = fetcher.get_all_symbols() +``` + +### 5.2 手动指定适配器 + +```python +from qihuo_analyzer.data.api_adapters.adapter_factory import DataAdapterFactory + +# 手动创建特定适配器 +adapter = DataAdapterFactory.create_adapter("rqdata") +adapter.connect() + +# 使用适配器获取数据 +data = adapter.get_kline_data("SHFE.rb2409", 3600, start_time, end_time) + +# 断开连接 +adapter.disconnect() +``` + +## 6. 配置说明 + +### 6.1 Windows环境变量配置 + +1. **打开系统属性**:右键"此电脑" → "属性" → "高级系统设置" +2. **环境变量**:点击"环境变量"按钮 +3. **添加系统变量**:在"系统变量"区域点击"新建" +4. **设置变量**: + - 变量名:`DATA_ADAPTER_TYPE` + - 变量值:`tqsdk` 或 `rqdata` +5. **设置账号变量**(可选): + - TQSDK:添加 `TQSDK_USERNAME` 和 `TQSDK_PASSWORD` + - RQData:添加 `RQDATA_USERNAME` 和 `RQDATA_PASSWORD` +6. **重启应用**:配置生效需要重启应用程序 + +### 6.2 临时配置 + +在Python代码中临时设置环境变量: + +```python +import os + +# 设置适配器类型 +os.environ["DATA_ADAPTER_TYPE"] = "rqdata" + +# 设置RQData账号 +os.environ["RQDATA_USERNAME"] = "your_username" +os.environ["RQDATA_PASSWORD"] = "your_password" + +# 然后创建DataFetcher实例 +from qihuo_analyzer.data.data_fetcher import DataFetcher +fetcher = DataFetcher() +``` + +## 7. 测试验证 + +### 7.1 测试脚本 + +**文件路径**:`test_data_adapters.py` + +测试脚本验证了所有适配器的功能: + +- 连接测试 +- 数据获取测试 +- 错误处理测试 +- 模拟数据测试 + +### 7.2 运行测试 + +```bash +# 在项目根目录运行测试 +python test_data_adapters.py +``` + +### 7.3 测试结果 + +测试输出示例: + +``` +=== 测试 tqsdk 适配器 === +创建TQSDK数据适配器 +连接API... +TQSDK账号密码未配置,将使用模拟数据 +连接结果: 失败 +API连接失败,跳过测试 + +=== 测试 rqdata 适配器 === +创建RQData数据适配器 +连接API... +RQData账号密码未配置,将使用模拟数据 +连接结果: 失败 +API连接失败,跳过测试 + +=== 测试完成 === +``` + +## 8. 未来扩展 + +### 8.1 添加新适配器 + +要添加新的数据源适配器,只需: + +1. **创建适配器类**:继承`BaseDataAdapter` +2. **实现接口方法**:实现所有抽象方法 +3. **注册适配器**:在工厂类中添加适配器类型 + +**示例**: + +```python +# 在adapter_factory.py中添加 +ADAPTERS = { + "tqsdk": TQSdkAdapter, + "rqdata": RQDataAdapter, + "new_adapter": NewAdapter # 新适配器 +} +``` + +### 8.2 增强功能 + +未来可考虑的增强功能: + +- **缓存机制**:添加数据缓存,减少重复API调用 +- **多数据源融合**:支持从多个数据源获取数据并融合 +- **数据源健康检查**:定期检查数据源可用性 +- **数据质量监控**:监控数据完整性和准确性 +- **异步数据获取**:支持异步API调用,提高性能 + +## 9. 故障排除 + +### 9.1 常见问题 + +1. **API连接失败** + - 检查账号配置是否正确 + - 检查网络连接 + - 检查API服务是否正常 + +2. **数据获取失败** + - 检查合约代码是否正确 + - 检查时间范围是否合理 + - 检查API权限是否足够 + +3. **适配器初始化失败** + - 检查环境变量配置 + - 检查依赖包是否安装 + - 检查Python版本是否兼容 + +### 9.2 日志与调试 + +- **日志级别**:可通过配置调整日志级别 +- **调试模式**:设置环境变量`DEBUG=True`启用调试模式 +- **错误信息**:详细的错误信息会记录到日志中 + +## 10. 依赖管理 + +### 10.1 核心依赖 + +| 依赖包 | 版本 | 用途 | +|--------|------|------| +| tqsdk | ^1.9.15 | TQSDK数据源支持 | +| rqdatac | ^2.0.0 | RQData数据源支持 | +| pandas | ^1.3.0 | 数据处理 | +| numpy | ^1.20.0 | 数值计算 | + +### 10.2 安装依赖 + +```bash +# 安装依赖包 +pip install -r requirements.txt +``` + +## 11. 总结 + +统一数据获取接口的实现成功解决了多数据源集成的问题,为AlphaFutures系统提供了: + +- **灵活性**:支持多种数据源,可根据需要切换 +- **可靠性**:包含错误处理和降级机制 +- **可扩展性**:支持轻松添加新的数据源 +- **一致性**:为业务逻辑提供统一的数据接口 +- **可维护性**:代码结构清晰,易于维护和测试 + +该设计不仅满足了当前的需求,也为未来的功能扩展和性能优化奠定了基础。 \ No newline at end of file