From b06e29a95c41903fd04ad913c7a075b571348bc1 Mon Sep 17 00:00:00 2001 From: Lxy Date: Mon, 16 Feb 2026 08:49:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=A2=9E=E5=8A=A0=E5=A4=9A=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E6=BA=90=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- # AI 期货分析系统运行指南.ini | 2 +- .env | 4 + SKILL.md | 18 + __pycache__/config.cpython-311.pyc | Bin 0 -> 428 bytes config.py | 29 ++ .../__pycache__/data_fetcher.cpython-311.pyc | Bin 19675 -> 17924 bytes qihuo_analyzer/data/api_adapters/__init__.py | 12 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 639 bytes .../adapter_factory.cpython-311.pyc | Bin 0 -> 2021 bytes .../__pycache__/base_adapter.cpython-311.pyc | Bin 0 -> 3674 bytes .../rqdata_adapter.cpython-311.pyc | Bin 0 -> 14195 bytes .../__pycache__/tqsdk_adapter.cpython-311.pyc | Bin 0 -> 13116 bytes .../data/api_adapters/adapter_factory.py | 38 ++ .../data/api_adapters/base_adapter.py | 85 ++++ .../data/api_adapters/rqdata_adapter.py | 396 ++++++++++++++++++ .../data/api_adapters/tqsdk_adapter.py | 335 +++++++++++++++ qihuo_analyzer/data/data_fetcher.py | 391 +++++++---------- requirements.txt | 3 +- test_config_switch.py | 122 ++++++ test_data_adapters.py | 85 ++++ test_enum_symbols.py | 123 ++++++ test_iron_ore_fix.py | 90 ++++ test_mock_symbols.py | 81 ++++ test_tq_symbol_fix.py | 102 +++++ web/__pycache__/auth.cpython-311.pyc | Bin 0 -> 5237 bytes 品种备份.ini | 49 +++ 数据源切换指南.md | 295 +++++++++++++ 统一数据获取接口开发文档.md | 354 ++++++++++++++++ 28 files changed, 2364 insertions(+), 250 deletions(-) create mode 100644 SKILL.md create mode 100644 __pycache__/config.cpython-311.pyc create mode 100644 config.py create mode 100644 qihuo_analyzer/data/api_adapters/__init__.py create mode 100644 qihuo_analyzer/data/api_adapters/__pycache__/__init__.cpython-311.pyc create mode 100644 qihuo_analyzer/data/api_adapters/__pycache__/adapter_factory.cpython-311.pyc create mode 100644 qihuo_analyzer/data/api_adapters/__pycache__/base_adapter.cpython-311.pyc create mode 100644 qihuo_analyzer/data/api_adapters/__pycache__/rqdata_adapter.cpython-311.pyc create mode 100644 qihuo_analyzer/data/api_adapters/__pycache__/tqsdk_adapter.cpython-311.pyc create mode 100644 qihuo_analyzer/data/api_adapters/adapter_factory.py create mode 100644 qihuo_analyzer/data/api_adapters/base_adapter.py create mode 100644 qihuo_analyzer/data/api_adapters/rqdata_adapter.py create mode 100644 qihuo_analyzer/data/api_adapters/tqsdk_adapter.py create mode 100644 test_config_switch.py create mode 100644 test_data_adapters.py create mode 100644 test_enum_symbols.py create mode 100644 test_iron_ore_fix.py create mode 100644 test_mock_symbols.py create mode 100644 test_tq_symbol_fix.py create mode 100644 web/__pycache__/auth.cpython-311.pyc create mode 100644 品种备份.ini create mode 100644 数据源切换指南.md create mode 100644 统一数据获取接口开发文档.md 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 0000000000000000000000000000000000000000..ec66a49aa0a1311c45769ac9af0a59db211e90ed GIT binary patch literal 428 zcmZ3^%ge<81P`B0%)G= z_!hUJg_(hYfsv`9p_$uDIiUF=nF4$a41Pe(f-a6Bj`5BzjsYRALGdAx0j{_BLIQ(b zyyHWIU4#4_ec}87$Kc>_{~(uJ{6T?WO>hM;0k{fQ7gwiH_gg~F{(gS0&LN)ue(@om zzOMeEA-8y3{T!WqT;rV`ojqJ{@qnp#*N6bmpvaXBpFv^pOWDOLraZqWySN}RIW@*H zrywKIt+b@HD782yIX^EgGhMHs@)w6qZhlH>PO4pzFi;OD@QPi4#0O?ZM#c|345Bg@ z7z96XFbF9$@ZAuUZQuj4g%uk3ZwSdX@B`VLd=0E0*ciklE-(lIxdI{$JP-JV8+bsl INDycc04~>dGXMYp literal 0 HcmV?d00001 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 f68926465d30d671bf553cc8169061882fcef841..1d3906cb89055df6449c2caf2d7d57e7ab10f6a0 100644 GIT binary patch literal 17924 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 literal 19675 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 IB}exE0X7J~ZU6uP 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 0000000000000000000000000000000000000000..69eba42026156306e029fca37d33396c7970f87f GIT binary patch literal 639 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 a`sLEEzy)a<;^9ok#QTKv_e1!^-+utJ^g$Z{ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3ee354955758da4210e5705aecbf41d5234e0e8a GIT binary patch literal 14195 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..64769aa07c6967f7eac4f56f69236f05007d2366 GIT binary patch literal 13116 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 y^Opu{jT^Nm^8}-T&$B*_J@iMC@~1nbha4~lZ 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 0000000000000000000000000000000000000000..8dccc9779b6bbdb51945046cad358de7cf0c038b GIT binary patch literal 5237 zcmbVQYj7LY6~6n>b0y1<4xEqf&;cUNP_ zm0Q7NK)8m4HY6bpjrq|KhM{rWAH2#ig@64eG25cqnanWLkukq2CS-=;SI^y*)?US7 z0$19z_i^s)-1D7t?!M`EI}xO9yIx6NgWP{fp`I+Y%+-HE<_zKyPl+f+MW_@Vp;JtR zNwE<&WrF`CF=)sg^~ zalch{1v6vG^k6I<8xyeVI3x*pFgcw}t4@(LM6*z#HtBgX9*o1Q9F#LzS#Na3vlt6$ zz2v9W4P!!Dz%f~f9*IfPR0i|WahOYOi;oNONpoc=Uf(TwQ?8A#f6a`@yK|r zKPzXkAdO53qa(4bJih(NG1VT8Ceukd8l7>IEo`qB-DI$>K=P>Ex~{;^+sa-ayxiq2 zcNAE~-d@!HSAhe?jkQC8EI`|L7i7*L+1MpQIW%FEa@0lW^|sM-7~wYP;HR+%UfNzD zzY+4Py}ogYaQl4NOKO|Ur#I84E#F+(3XBqwO&(qG{A8h9>3Cpqy5!lTc=i7N!}Avs8{+Sltu&^~8hXB^rdiV2HiW zMcxe~(l8>K`?_T2qfm~<9kOGEEl2Y-5tp8z#F$F#O37?(hQSH}n5m z-;?_SLjhp+{ld4q0W(AeTE+x9%8y3lnRFWLQZkcPZNjt=&&q;oPY7~+Jd+kwOI*wV zNXRh(JZ)s!C}5N&y;PPIB_c6^pRS`0F%#o8qlE8(dZLL^8b}^pvm)1)61P?1w&wfF zoae;Da}O82;o_6OEI#q85*Jgr*yr5DXWT@I6BSMb9@hTC{^#~T8$2G&2g^2&DAE9R z#nX1O@9er$`xn@Sh~m3vv2*da%6)rFo_&gEU(vn~MlN(~x)q0MjYJ%drdI!f45*dS zfL0-NZ6&z@@0yAt+gAp@2+H}kp`bj))8@mUMKZU-Jz=fqbI9CtmXb9`s<%(r3_dDq z%$B1-jhHME_wvi{R!*E=dUX+$sq(=emOuW}(#wBVSw1GmX4(?EJ&b~{5syhpX*)ma zcc^qmQrROhd0b^DGT?V?;HoqO3}zyc)J%g-Iw~e*;eORt(-)N)%1o&o&P+uUvACSU z$H4E{K%dAs_;y&xYTJc7ph6?Jl9qlTc~og?o*z3Q&WW$zcmBZ(yU*=@XGiIlZsnHl z#YFLu;ZjprX$t4}Ug28jcb|Q*#C0fKN51dM#;vD*o$piFb#T)81J|E4IT!7u&qL;W z4G3808yOJu2>{^H+PZO_VL&%$KzLd^uTEHErSN*?&EHk#SB~s||Cp~7{#g0&10DU# zZ(N%B*a-;JQtwcpCmisFdmar2d;|Ty(7~`T@YKL?c(?}p+YMI25AYudg#&v7L%zp` z1_pbE4*L!S4*Plzg%1vdV64GFDBNZ0QMW9>{zwNGpSTk#y^FEpVF0u0HtO_Y?^!KNAXUmBRtYRAfPYYX2X7f zZ3sRWtRicx{cB{kIKCkSia?r>#qq*{=MFqOcziHFXtE-Cuq3+1tab;6ZD@zu@6{OE z0jB>(I`bo)4wGZ>Ms3_RElQ(?5zR5V9?Gn%v+NC52r*ZVP0;2jggCQ>dN>YZUP+BJ z|9~|@DBOW$5+T%5C&In67MRJfUiB98Zk&m?$i(5)QqE%DVSRa#Ow`ED7w)FQ)mFMS|PA}@{ocWpIm9KZc2A04D`|18H ziOy;+{p3rPcmGy-=WONtrJ0=rq2a(#m>~6F9mVyo4G!M#d!i?JC@}21eRmg(>8Hb4 zBIeqn3MVUb7y3hf#|qf+HfYw6hMP!k2N2Z}n2rlaG@ozB8BrXK#V1uplCj^a2iBVj zD>34#TbNEtaxy)pJ9QFqtCS#F;jP)D8fM*C7y~|t4#>js9%#NF9;p>@Z9;C9Zn3QY z!n^r`>zu2&EmVB`aPhIHOWqO1JMy_#_{=Mmykm-Ytl%zlUhw#iyUT6ctL@0)KH;2m zzUZ2F6|~>A^=Q-Gm!?bJ9g25H!Ckcy4%eKkxc;%?@YBU-CW=FoCA+BD#iCs-+g&HP zIqpUGyc@>#bX1+!jrZ*}2Xd{iBHFTLg^+&kIrod6c~8Mp_N;p~e)hIk5~mV{*`j^( z>i-wOxN3RdLaI7oC@o`em<%eV2S7F_?|ifq{j>EUpr3R)d+%jF+1}aP#eCXILH^S= zwznI7+GQd6dl{1N-q7b{OcxJw2%4+w1}T5P;R0nhLJ{a?q#qqCGdBw#FmL-!1`Do{ zEAepSr3iPg4vKP&DUxRRRgpBbts18xv|?p)o2#W9Yc?S8jR2x<-U=?*mL(49tNgj; zk6%~;=I`e#m!JP~;n^==`>^uSt2LbOF>w9^qVGD2wHTaWXC)A?Lsy?20mIl2wfeO>ni-vth_%a;`Wbh$#7PP#H3aZq zkN}>sbpE@!W1;WTtxD(4_jZ(;`;_KBt@=va#2-zhj2*JeXs2us)G8xtO zib>pz$^A_e7$#vjCT0{9tx;R11Uxw-WXHBka2>))`Iv4xH8I~u*zO{1Ze4CE{kx5J zMN&C(;4n#y2O~%>K>B%+Kt{8Pnndw!q)4vf+5voLe^|ePKZ1#@2~4HCfmRucqQJ0G z4;9g_oB5YfqWDApWi(J+>t9AqdF@w5j=c7RRkUn5>3XH)~`(qJHOUi zzFFPgifE5QwO45y)l51zf6XBJ7Ne%sLaGhY#8E9(WRUiC6c}cMbUCTJs_1&63H1Lc C3b+{n literal 0 HcmV?d00001 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