diff --git a/backend/service_implementation/qihuo_analyzer/data/__pycache__/data_fetcher.cpython-311.pyc b/backend/service_implementation/qihuo_analyzer/data/__pycache__/data_fetcher.cpython-311.pyc index 7b25fc1..96481ce 100644 Binary files a/backend/service_implementation/qihuo_analyzer/data/__pycache__/data_fetcher.cpython-311.pyc and b/backend/service_implementation/qihuo_analyzer/data/__pycache__/data_fetcher.cpython-311.pyc differ diff --git a/backend/service_implementation/qihuo_analyzer/data/api_adapters/__pycache__/adapter_factory.cpython-311.pyc b/backend/service_implementation/qihuo_analyzer/data/api_adapters/__pycache__/adapter_factory.cpython-311.pyc index ef12c13..fe2ee8d 100644 Binary files a/backend/service_implementation/qihuo_analyzer/data/api_adapters/__pycache__/adapter_factory.cpython-311.pyc and b/backend/service_implementation/qihuo_analyzer/data/api_adapters/__pycache__/adapter_factory.cpython-311.pyc differ diff --git a/backend/service_implementation/qihuo_analyzer/data/api_adapters/__pycache__/akshare_adapter.cpython-311.pyc b/backend/service_implementation/qihuo_analyzer/data/api_adapters/__pycache__/akshare_adapter.cpython-311.pyc new file mode 100644 index 0000000..29d2617 Binary files /dev/null and b/backend/service_implementation/qihuo_analyzer/data/api_adapters/__pycache__/akshare_adapter.cpython-311.pyc differ diff --git a/backend/service_implementation/qihuo_analyzer/data/api_adapters/__pycache__/tqsdk_adapter.cpython-311.pyc b/backend/service_implementation/qihuo_analyzer/data/api_adapters/__pycache__/tqsdk_adapter.cpython-311.pyc index 9cb0979..70dde53 100644 Binary files a/backend/service_implementation/qihuo_analyzer/data/api_adapters/__pycache__/tqsdk_adapter.cpython-311.pyc and b/backend/service_implementation/qihuo_analyzer/data/api_adapters/__pycache__/tqsdk_adapter.cpython-311.pyc differ diff --git a/backend/service_implementation/qihuo_analyzer/data/api_adapters/adapter_factory.py b/backend/service_implementation/qihuo_analyzer/data/api_adapters/adapter_factory.py index 1806356..7eeb34d 100644 --- a/backend/service_implementation/qihuo_analyzer/data/api_adapters/adapter_factory.py +++ b/backend/service_implementation/qihuo_analyzer/data/api_adapters/adapter_factory.py @@ -4,6 +4,7 @@ import os 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.akshare_adapter import AkShareAdapter class DataAdapterFactory: @@ -38,7 +39,7 @@ class DataAdapterFactory: """创建数据适配器 Args: - adapter_type: 适配器类型,可选值:'tqsdk', 'rqdata'。如果为None,则从配置文件获取。 + adapter_type: 适配器类型,可选值:'tqsdk', 'rqdata', 'akshare'。如果为None,则从配置文件获取。 Returns: BaseDataAdapter: 数据适配器实例 @@ -55,9 +56,31 @@ class DataAdapterFactory: # 根据类型创建适配器 if adapter_type == 'tqsdk': + # 检查tqsdk是否启用 + tqsdk_config = data_source_config.get('tqsdk', {}) + tqsdk_enabled = tqsdk_config.get('enabled', False) + if not tqsdk_enabled: + print("TQSDK未启用,尝试使用其他启用的数据源") + # 尝试使用akshare + akshare_config = data_source_config.get('akshare', {}) + akshare_enabled = akshare_config.get('enabled', False) + if akshare_enabled: + print("使用AkShare适配器") + use_cache = akshare_config.get('use_cache', True) + return AkShareAdapter(use_cache=use_cache) + # 尝试使用rqdata + rqdata_config = data_source_config.get('rqdata', {}) + rqdata_enabled = rqdata_config.get('enabled', False) + if rqdata_enabled: + print("使用RQData适配器") + username = rqdata_config.get('username', '') + password = rqdata_config.get('password', '') + return RqDataAdapter(username=username, password=password) + # 所有数据源都未启用,返回AkShare适配器 + print("所有数据源都未启用,返回AkShare适配器") + return AkShareAdapter() print("创建TQSDK数据适配器") # 获取TQSDK配置 - tqsdk_config = data_source_config.get('tqsdk', {}) username = tqsdk_config.get('username', '') password = tqsdk_config.get('password', '') return TqSdkAdapter(username=username, password=password) @@ -68,6 +91,12 @@ class DataAdapterFactory: username = rqdata_config.get('username', '') password = rqdata_config.get('password', '') return RqDataAdapter(username=username, password=password) + elif adapter_type == 'akshare': + print("创建AkShare数据适配器") + # 获取AkShare配置 + akshare_config = data_source_config.get('akshare', {}) + use_cache = akshare_config.get('use_cache', True) + return AkShareAdapter(use_cache=use_cache) else: # 默认使用TQSDK适配器 print(f"未知的适配器类型:{adapter_type},使用默认的TQSDK适配器") @@ -75,4 +104,49 @@ class DataAdapterFactory: tqsdk_config = data_source_config.get('tqsdk', {}) username = tqsdk_config.get('username', '') password = tqsdk_config.get('password', '') - return TqSdkAdapter(username=username, password=password) \ No newline at end of file + return TqSdkAdapter(username=username, password=password) + + @staticmethod + def get_available_adapters() -> list: + """获取所有可用的适配器类型 + + Returns: + list: 适配器类型列表 + """ + return ['tqsdk', 'rqdata', 'akshare'] + + @staticmethod + def test_adapter(adapter_type: str) -> dict: + """测试适配器连接 + + Args: + adapter_type: 适配器类型 + + Returns: + dict: 测试结果 + """ + try: + adapter = DataAdapterFactory.create_adapter(adapter_type) + connected = adapter.connect() + + if connected: + # 测试获取数据 + symbols = adapter.get_all_symbols() + adapter.disconnect() + + return { + 'success': True, + 'message': f'{adapter_type} 连接成功,支持 {len(symbols)} 个品种', + 'symbols_count': len(symbols) + } + else: + return { + 'success': False, + 'message': f'{adapter_type} 连接失败' + } + + except Exception as e: + return { + 'success': False, + 'message': f'{adapter_type} 测试失败:{str(e)}' + } diff --git a/backend/service_implementation/qihuo_analyzer/data/api_adapters/akshare_adapter.py b/backend/service_implementation/qihuo_analyzer/data/api_adapters/akshare_adapter.py new file mode 100644 index 0000000..d366228 --- /dev/null +++ b/backend/service_implementation/qihuo_analyzer/data/api_adapters/akshare_adapter.py @@ -0,0 +1,457 @@ +# AkShare数据适配器 +import akshare as ak +import pandas as pd +import numpy as np +from typing import Dict, Optional, List +from datetime import datetime, timedelta +from qihuo_analyzer.data.api_adapters.base_adapter import BaseDataAdapter + + +class AkShareAdapter(BaseDataAdapter): + """AkShare数据适配器 + + 使用AkShare库获取国内期货数据,AkShare是一个开源的金融数据接口库。 + """ + + def __init__(self, use_cache: bool = True): + """初始化AkShare适配器 + + Args: + use_cache: 是否使用缓存 + """ + self.connected = False + self.use_cache = use_cache + self._cache = {} + + # 品种代码映射(AkShare格式 -> 内部格式) + self.symbol_mapping = { + 'AU': 'AU', # 黄金 + 'AG': 'AG', # 白银 + 'CU': 'CU', # 铜 + 'NI': 'NI', # 镍 + 'SN': 'SN', # 锡 + 'AL': 'AL', # 铝 + 'ZN': 'ZN', # 锌 + 'PB': 'PB', # 铅 + 'FG': 'FG', # 玻璃 + 'RB': 'RB', # 螺纹钢 + 'HC': 'HC', # 热轧卷板 + 'JM': 'JM', # 焦煤 + 'J': 'J', # 焦炭 + 'I': 'I', # 铁矿石 + 'MA': 'MA', # 甲醇 + 'V': 'V', # PVC + 'TA': 'TA', # PTA + 'SC': 'SC', # 原油 + 'RU': 'RU', # 橡胶 + 'P': 'P', # 棕榈油 + 'Y': 'Y', # 豆油 + 'M': 'M', # 豆粕 + 'C': 'C', # 玉米 + 'CF': 'CF', # 棉花 + 'SR': 'SR', # 白糖 + 'LI': 'LC', # 碳酸锂(AkShare中为LC) + } + + # 交易所映射 + self.exchange_mapping = { + 'AU': 'SHFE', 'AG': 'SHFE', 'CU': 'SHFE', 'NI': 'SHFE', 'SN': 'SHFE', + 'AL': 'SHFE', 'ZN': 'SHFE', 'PB': 'SHFE', 'RB': 'SHFE', 'HC': 'SHFE', + 'SC': 'INE', 'LU': 'INE', 'NR': 'INE', + 'FG': 'CZCE', 'MA': 'CZCE', 'TA': 'CZCE', 'CF': 'CZCE', 'SR': 'CZCE', + 'RM': 'CZCE', 'OI': 'CZCE', + 'V': 'DCE', 'P': 'DCE', 'Y': 'DCE', 'M': 'DCE', 'C': 'DCE', 'I': 'DCE', + 'J': 'DCE', 'JM': 'DCE', 'L': 'DCE', 'PP': 'DCE', + 'IM': 'CFFEX', 'IC': 'CFFEX', 'IH': 'CFFEX', + } + + def connect(self) -> bool: + """连接AkShare + + Returns: + bool: 连接是否成功(AkShare不需要显式连接,直接返回True) + """ + try: + # 测试获取期货品种列表,验证AkShare是否可用 + # 尝试不传递symbol参数或使用其他参数 + try: + # 尝试获取所有期货实时行情 + test_df = ak.futures_zh_realtime() + except: + # 如果失败,尝试获取特定品种 + test_df = ak.futures_zh_realtime(symbol="AU") + self.connected = True + print("AkShare连接成功") + return True + except Exception as e: + print(f"AkShare连接失败:{e}") + self.connected = False + return False + + def disconnect(self): + """断开连接(AkShare不需要显式断开)""" + self.connected = False + print("AkShare已断开") + + def _convert_duration(self, duration: str) -> str: + """转换时间周期格式 + + Args: + duration: 时间周期,如 '1m', '5m', '15m', '1h', '1d' + + Returns: + AkShare支持的时间周期 + """ + duration_map = { + '1m': '1', + '5m': '5', + '15m': '15', + '30m': '30', + '1h': '60', + '4h': '240', + '1d': 'D', + } + return duration_map.get(duration, '1d') + + def _get_full_symbol(self, symbol: str) -> str: + """获取完整的合约代码(带交易所) + + Args: + symbol: 品种代码,如 'AU' + + Returns: + 完整合约代码,如 'SHFE.AU2506' + """ + exchange = self.exchange_mapping.get(symbol, 'SHFE') + # 获取主力合约月份 + try: + df = ak.futures_zh_realtime(symbol=symbol) + if not df.empty and 'symbol' in df.columns: + main_contract = df['symbol'].iloc[0] + return f"{exchange}.{main_contract}" + except: + pass + + # 默认返回当前月份+1的合约 + today = datetime.now() + contract_month = (today + timedelta(days=30)).strftime('%y%m') + return f"{exchange}.{symbol}{contract_month}" + + def get_kline_data(self, symbol: str, duration: str = '1d', count: int = 200) -> Optional[pd.DataFrame]: + """获取K线数据 + + Args: + symbol: 合约代码,如 'AU' + duration: 时间周期,如 '1m', '5m', '15m', '1h', '1d' + count: 数据数量 + + Returns: + K线数据DataFrame + """ + if not self.connected: + self.connect() + + try: + # 获取主力合约代码 + full_symbol = self._get_full_symbol(symbol) + + # 计算起始日期 + end_date = datetime.now() + if duration.endswith('m'): + minutes = int(duration[:-1]) + start_date = end_date - timedelta(minutes=minutes * count) + elif duration.endswith('h'): + hours = int(duration[:-1]) + start_date = end_date - timedelta(hours=hours * count) + else: + start_date = end_date - timedelta(days=count) + + # 获取K线数据 + # 注意:AkShare的期货历史数据接口可能需要特定格式 + # 这里使用 futures_zh_daily 或 futures_zh_minute_sina + + if duration in ['1d', 'D']: + # 日K线数据 + df = ak.futures_zh_daily(symbol=full_symbol, start_date=start_date.strftime('%Y%m%d')) + else: + # 分钟K线数据(使用新浪接口) + # 需要先获取完整合约代码 + df = ak.futures_zh_minute_sina(symbol=full_symbol, period=self._convert_duration(duration)) + + if df is None or df.empty: + print(f"未获取到 {symbol} 的K线数据") + return None + + # 统一列名格式 + column_mapping = { + '日期': 'datetime', + '开盘': 'open', + '收盘': 'close', + '最高': 'high', + '最低': 'low', + '成交量': 'volume', + 'date': 'datetime', + 'open': 'open', + 'close': 'close', + 'high': 'high', + 'low': 'low', + 'volume': 'volume', + } + + # 重命名列 + for old_col, new_col in column_mapping.items(): + if old_col in df.columns: + df[new_col] = df[old_col] + + # 确保必要列存在 + required_cols = ['datetime', 'open', 'close', 'high', 'low', 'volume'] + for col in required_cols: + if col not in df.columns: + df[col] = np.nan + + # 转换datetime格式 + if 'datetime' in df.columns: + df['datetime'] = pd.to_datetime(df['datetime']) + + # 按时间排序 + df = df.sort_values('datetime') + + # 限制数据量 + df = df.tail(count) + + # 检查数据长度 + if len(df) == 0: + print(f"获取到的K线数据为空,合约: {symbol}") + return None + + # 检查所有必要列是否存在且有值 + for col in required_cols: + if col not in df.columns: + print(f"数据缺少列: {col}") + return None + if df[col].isnull().all(): + print(f"列 {col} 所有值都为NaN") + return None + + return df[required_cols] + + except Exception as e: + print(f"获取 {symbol} 的K线数据失败:{e}") + return None + + def get_tick_data(self, symbol: str, count: int = 1000) -> Optional[pd.DataFrame]: + """获取Tick数据 + + Args: + symbol: 合约代码 + count: 数据数量 + + Returns: + Tick数据DataFrame + """ + if not self.connected: + self.connect() + + try: + # AkShare的Tick数据接口有限,这里使用实时行情模拟 + full_symbol = self._get_full_symbol(symbol) + df = ak.futures_zh_realtime(symbol=symbol) + + if df is None or df.empty: + return None + + # 转换格式 + tick_df = pd.DataFrame({ + 'datetime': [datetime.now()], + 'last_price': [float(df['last_price'].iloc[0]) if 'last_price' in df.columns else 0], + 'volume': [int(df['volume'].iloc[0]) if 'volume' in df.columns else 0], + 'bid_price_1': [float(df['bid_price'].iloc[0]) if 'bid_price' in df.columns else 0], + 'ask_price_1': [float(df['ask_price'].iloc[0]) if 'ask_price' in df.columns else 0], + 'bid_volume_1': [int(df['bid_volume'].iloc[0]) if 'bid_volume' in df.columns else 0], + 'ask_volume_1': [int(df['ask_volume'].iloc[0]) if 'ask_volume' in df.columns else 0], + }) + + return tick_df + + except Exception as e: + print(f"获取 {symbol} 的Tick数据失败:{e}") + return None + + def get_contract_info(self, symbol: str) -> Optional[Dict]: + """获取合约信息 + + Args: + symbol: 合约代码 + + Returns: + 合约信息字典 + """ + if not self.connected: + self.connect() + + try: + # 获取实时行情作为合约信息 + df = ak.futures_zh_realtime(symbol=symbol) + + if df is None or df.empty: + return None + + row = df.iloc[0] + + return { + 'symbol': symbol, + 'name': row.get('name', symbol), + 'exchange': self.exchange_mapping.get(symbol, 'SHFE'), + 'last_price': float(row.get('last_price', 0)), + 'change_percent': float(row.get('change_percent', 0)), + 'volume': int(row.get('volume', 0)), + 'open_interest': int(row.get('open_interest', 0)), + 'settlement': float(row.get('settlement', 0)), + 'pre_settlement': float(row.get('pre_settlement', 0)), + } + + except Exception as e: + print(f"获取 {symbol} 的合约信息失败:{e}") + return None + + def get_market_data(self, symbols: List[str]) -> Dict[str, Dict]: + """批量获取市场数据 + + Args: + symbols: 合约代码列表 + + Returns: + 市场数据字典 + """ + if not self.connected: + self.connect() + + market_data = {} + + try: + # 获取所有期货实时行情 + df = ak.futures_zh_realtime(symbol="主力") + + if df is None or df.empty: + return market_data + + for symbol in symbols: + try: + # 过滤出对应品种的数据 + symbol_data = df[df['symbol'].str.contains(symbol, case=False, na=False)] + + if not symbol_data.empty: + row = symbol_data.iloc[0] + market_data[symbol] = { + 'symbol': symbol, + 'name': row.get('name', symbol), + 'last_price': float(row.get('last_price', 0)), + 'change_percent': float(row.get('change_percent', 0)), + 'change': float(row.get('change', 0)), + 'volume': int(row.get('volume', 0)), + 'amount': float(row.get('amount', 0)), + 'open_interest': int(row.get('open_interest', 0)), + 'high': float(row.get('high', 0)), + 'low': float(row.get('low', 0)), + 'open': float(row.get('open', 0)), + 'settlement': float(row.get('settlement', 0)), + 'pre_settlement': float(row.get('pre_settlement', 0)), + 'bid_price': float(row.get('bid_price', 0)), + 'ask_price': float(row.get('ask_price', 0)), + 'bid_volume': int(row.get('bid_volume', 0)), + 'ask_volume': int(row.get('ask_volume', 0)), + } + except Exception as e: + print(f"处理 {symbol} 的市场数据失败:{e}") + continue + + return market_data + + except Exception as e: + print(f"获取市场数据失败:{e}") + return market_data + + def get_all_symbols(self) -> List[str]: + """获取所有品种列表 + + Returns: + 所有品种的合约代码列表 + """ + # 返回支持的期货品种列表 + return list(self.symbol_mapping.keys()) + + def get_futures_list(self) -> pd.DataFrame: + """获取期货品种列表(详细) + + Returns: + 期货品种DataFrame + """ + try: + df = ak.futures_zh_realtime(symbol="主力") + return df + except Exception as e: + print(f"获取期货品种列表失败:{e}") + return pd.DataFrame() + + def get_main_contract(self, symbol: str) -> Optional[str]: + """获取主力合约代码 + + Args: + symbol: 品种代码 + + Returns: + 主力合约代码 + """ + try: + df = ak.futures_zh_realtime(symbol=symbol) + if not df.empty and 'symbol' in df.columns: + return df['symbol'].iloc[0] + return None + except Exception as e: + print(f"获取 {symbol} 的主力合约失败:{e}") + return None + + def get_main_contracts(self) -> Dict[str, str]: + """获取所有品种的主力合约 + + Returns: + 品种代码到主力合约代码的映射 + """ + try: + # 尝试获取所有期货实时行情 + try: + df = ak.futures_zh_realtime() + except: + # 如果失败,返回空字典 + return {} + + if df is None or df.empty: + return {} + + main_contracts = {} + for _, row in df.iterrows(): + if 'symbol' in row: + # 提取品种代码(前2个字符) + symbol = row['symbol'][:2] + main_contracts[symbol] = row['symbol'] + + return main_contracts + except Exception as e: + print(f"获取主力合约失败:{e}") + return {} + + def get_last_trading_day(self) -> Optional[str]: + """获取最后一个交易日 + + Returns: + str: 最后一个交易日,格式为 'YYYY-MM-DD' + """ + try: + # 使用当前日期的前一天作为最后交易日 + # 实际项目中,应该使用AkShare的交易日历接口 + from datetime import datetime, timedelta + last_trading_day = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d') + return last_trading_day + except Exception as e: + print(f"获取最后交易日失败:{e}") + return None diff --git a/backend/service_implementation/qihuo_analyzer/data/api_adapters/tqsdk_adapter.py b/backend/service_implementation/qihuo_analyzer/data/api_adapters/tqsdk_adapter.py index 4a8f854..a967d4f 100644 --- a/backend/service_implementation/qihuo_analyzer/data/api_adapters/tqsdk_adapter.py +++ b/backend/service_implementation/qihuo_analyzer/data/api_adapters/tqsdk_adapter.py @@ -5,14 +5,23 @@ import pandas as pd from typing import Dict, Optional, List from qihuo_analyzer.data.api_adapters.base_adapter import BaseDataAdapter -# 尝试导入tqsdk +# 尝试导入tqsdk(仅当需要时) +TQSDK_AVAILABLE = False -try: - from tqsdk import TqApi, TqAuth - TQSDK_AVAILABLE = True -except Exception as e: - print(f"tqsdk导入失败:{e},将使用模拟数据") - TQSDK_AVAILABLE = False +# 延迟导入,仅在需要时导入 +def _try_import_tqsdk(): + """尝试导入tqsdk""" + global TQSDK_AVAILABLE + if not TQSDK_AVAILABLE: + try: + from tqsdk import TqApi, TqAuth + TQSDK_AVAILABLE = True + return True + except Exception as e: + print(f"tqsdk导入失败:{e},将使用模拟数据") + TQSDK_AVAILABLE = False + return False + return True class TqSdkAdapter(BaseDataAdapter): @@ -41,12 +50,20 @@ class TqSdkAdapter(BaseDataAdapter): bool: 连接是否成功 """ try: + # 尝试导入tqsdk + if not _try_import_tqsdk(): + # TQSDK不可用 + print("TQSDK不可用,使用模拟API") + self.api = None + return False + if TQSDK_AVAILABLE: # 使用天勤TQSDK连接 username = self.username or os.getenv('TQSDK_USERNAME', '') password = self.password or os.getenv('TQSDK_PASSWORD', '') if username and password: + from tqsdk import TqApi, TqAuth self.api = TqApi(auth=TqAuth(username, password)) print("TQSDK API连接成功") return True @@ -148,30 +165,45 @@ class TqSdkAdapter(BaseDataAdapter): 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 + # 检查TQSDK是否可用且已连接 + if not TQSDK_AVAILABLE or not self.api: + print(f"无法获取真实数据:{'API未连接' if not self.api else 'TQSDK不可用'}") + return None + + # 转换合约代码为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) + + # 检查所有需要的字段是否存在且长度一致 + required_fields = ['datetime', 'open', 'high', 'low', 'close', 'volume', 'open_oi'] + for field in required_fields: + if not hasattr(klines, field): + print(f"K线数据缺少字段: {field}") + return None + if len(getattr(klines, field)) != len(klines.datetime): + print(f"字段长度不匹配: {field} 长度为 {len(getattr(klines, field))}, datetime 长度为 {len(klines.datetime)}") + return None + + # 转换为DataFrame + try: data = { 'datetime': klines.datetime, 'open': klines.open, @@ -187,9 +219,8 @@ class TqSdkAdapter(BaseDataAdapter): print(f"成功获取K线数据,数据长度: {len(df)}") return df - else: - # 不再自动返回模拟数据,返回None - print(f"无法获取真实数据:{'API未连接' if not self.api else 'TQSDK不可用'}") + except Exception as e: + print(f"创建DataFrame失败:{e}") return None except Exception as e: print(f"tqsdk获取K线数据失败:{e}") @@ -199,31 +230,31 @@ class TqSdkAdapter(BaseDataAdapter): 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: - # 返回模拟数据 + # 检查TQSDK是否可用且已连接 + if not TQSDK_AVAILABLE or not self.api: print(f"无法获取真实数据:{'API未连接' if not self.api else 'TQSDK不可用'}") return None + + # 使用真实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 except Exception as e: print(f"获取Tick数据失败:{e}") return None @@ -231,26 +262,26 @@ class TqSdkAdapter(BaseDataAdapter): 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: - # 返回模拟数据 + # 检查TQSDK是否可用且已连接 + if not TQSDK_AVAILABLE or not self.api: print(f"无法获取真实数据:{'API未连接' if not self.api else 'TQSDK不可用'}") return None + + # 使用真实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 + } except Exception as e: print(f"获取合约信息失败:{e}") return None @@ -259,36 +290,28 @@ class TqSdkAdapter(BaseDataAdapter): """批量获取市场数据""" market_data = {} + # 检查TQSDK是否可用且已连接 + if not TQSDK_AVAILABLE or not self.api: + print(f"无法获取真实数据:{'API未连接' if not self.api else 'TQSDK不可用'}") + # 返回空字典 + return 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 - } + 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 + } except Exception as e: print(f"获取{symbol}市场数据失败:{e}") market_data[symbol] = { @@ -331,51 +354,51 @@ class TqSdkAdapter(BaseDataAdapter): Dict[str, str]: 品种代码到主力合约代码的映射 """ try: - if TQSDK_AVAILABLE and self.api: - # 使用TQSDK的query_quotes方法获取主力合约 - main_contracts = {} - - # 尝试获取不同类别的主力合约 - for ins_class in ['FUTURE']: - try: - # 查询主力合约 - quotes = self.api.query_quotes(ins_class=ins_class) - - # 等待数据准备就绪 - import time - start_time = time.time() - timeout = 5 # 5秒超时 - - while True: - if quotes: - break - if time.time() - start_time > timeout: - print("获取主力合约数据超时") - break - time.sleep(0.1) - - # 处理获取到的主力合约 - for quote in quotes: - try: - # 获取合约信息 - contract_info = self.api.get_quote(quote) - self.api.wait_update() - - if hasattr(contract_info, 'underlying_symbol') and hasattr(contract_info, 'instrument_id'): - underlying_symbol = contract_info.underlying_symbol - instrument_id = contract_info.instrument_id - main_contracts[underlying_symbol] = instrument_id - except Exception as e: - print(f"处理主力合约 {quote} 失败:{e}") - except Exception as e: - print(f"获取 {ins_class} 类别的主力合约失败:{e}") - - print(f"获取到主力合约:{main_contracts}") - return main_contracts - else: - # 返回模拟数据 + # 检查TQSDK是否可用且已连接 + if not TQSDK_AVAILABLE or not self.api: print("无法获取真实主力合约数据,使用模拟数据") return self._get_mock_main_contracts() + + # 使用TQSDK的query_quotes方法获取主力合约 + main_contracts = {} + + # 尝试获取不同类别的主力合约 + for ins_class in ['FUTURE']: + try: + # 查询主力合约 + quotes = self.api.query_quotes(ins_class=ins_class) + + # 等待数据准备就绪 + import time + start_time = time.time() + timeout = 5 # 5秒超时 + + while True: + if quotes: + break + if time.time() - start_time > timeout: + print("获取主力合约数据超时") + break + time.sleep(0.1) + + # 处理获取到的主力合约 + for quote in quotes: + try: + # 获取合约信息 + contract_info = self.api.get_quote(quote) + self.api.wait_update() + + if hasattr(contract_info, 'underlying_symbol') and hasattr(contract_info, 'instrument_id'): + underlying_symbol = contract_info.underlying_symbol + instrument_id = contract_info.instrument_id + main_contracts[underlying_symbol] = instrument_id + except Exception as e: + print(f"处理主力合约 {quote} 失败:{e}") + except Exception as e: + print(f"获取 {ins_class} 类别的主力合约失败:{e}") + + print(f"获取到主力合约:{main_contracts}") + return main_contracts except Exception as e: print(f"获取主力合约失败:{e}") return self._get_mock_main_contracts() @@ -401,62 +424,62 @@ class TqSdkAdapter(BaseDataAdapter): str: 最后一个交易日,格式为 'YYYY-MM-DD' """ try: - if TQSDK_AVAILABLE and self.api: - - # 计算日期范围 + # 检查TQSDK是否可用且已连接 + if not TQSDK_AVAILABLE or not self.api: + # 返回模拟数据 + print("无法获取真实数据,使用模拟最后交易日") + # 模拟返回前一天的日期 import datetime - end_date = datetime.datetime.now() - start_date = end_date - datetime.timedelta(days=10) - - # 格式化日期为字符串 - start_date_str = start_date.strftime('%Y%m%d') - end_date_str = end_date.strftime('%Y%m%d') + last_trading_day = (datetime.datetime.now() - datetime.timedelta(days=1)).strftime('%Y-%m-%d') + print(f"使用模拟最后交易日: {last_trading_day}") + return last_trading_day + + # 计算日期范围 + import datetime + end_date = datetime.datetime.now() + start_date = end_date - datetime.timedelta(days=10) + + # 格式化日期为字符串 + start_date_str = start_date.strftime('%Y%m%d') + end_date_str = end_date.strftime('%Y%m%d') + + print(f"获取交易日历,开始日期: {start_date_str}, 结束日期: {end_date_str}") + + # 使用get_trading_calendar获取交易日历 + try: + # 获取交易日历 + calendar = self.api.get_trading_calendar(start_date_str, end_date_str) - print(f"获取交易日历,开始日期: {start_date_str}, 结束日期: {end_date_str}") + # 等待数据准备就绪 + import time + start_time = time.time() + timeout = 5 # 5秒超时 - # 使用get_trading_calendar获取交易日历 - try: - # 获取交易日历 - calendar = self.api.get_trading_calendar(start_date_str, end_date_str) - - # 等待数据准备就绪 - import time - start_time = time.time() - timeout = 5 # 5秒超时 - - while True: - if hasattr(calendar, 'dates') and len(calendar.dates) > 0: - break - if time.time() - start_time > timeout: - print("获取交易日历超时") - # 回退到K线数据方法 - return "" - time.sleep(0.1) - - # 处理交易日历数据 + while True: if hasattr(calendar, 'dates') and len(calendar.dates) > 0: - # 获取最后一个交易日 - last_trading_date = calendar.dates[-1] - # 转换为YYYY-MM-DD格式 - last_trading_day = last_trading_date.strftime('%Y-%m-%d') - print(f"从交易日历获取最后交易日: {last_trading_day}") - return last_trading_day - else: - print("交易日历数据为空") + break + if time.time() - start_time > timeout: + print("获取交易日历超时") # 回退到K线数据方法 return "" - except Exception as calendar_error: - print(f"使用get_trading_calendar失败:{calendar_error}") + time.sleep(0.1) + + # 处理交易日历数据 + if hasattr(calendar, 'dates') and len(calendar.dates) > 0: + # 获取最后一个交易日 + last_trading_date = calendar.dates[-1] + # 转换为YYYY-MM-DD格式 + last_trading_day = last_trading_date.strftime('%Y-%m-%d') + print(f"从交易日历获取最后交易日: {last_trading_day}") + return last_trading_day + else: + print("交易日历数据为空") # 回退到K线数据方法 return "" - else: - # 返回模拟数据 - print("无法获取真实数据,使用模拟最后交易日") - # 模拟返回前一天的日期 - import datetime - last_trading_day = (datetime.datetime.now() - datetime.timedelta(days=1)).strftime('%Y-%m-%d') - print(f"使用模拟最后交易日: {last_trading_day}") - return last_trading_day + except Exception as calendar_error: + print(f"使用get_trading_calendar失败:{calendar_error}") + # 回退到K线数据方法 + return "" except Exception as e: print(f"获取最后交易日失败:{e}") # 返回模拟数据 diff --git a/backend/service_implementation/qihuo_analyzer/data/data_fetcher.py b/backend/service_implementation/qihuo_analyzer/data/data_fetcher.py index 1b5d08d..8cafaf9 100644 --- a/backend/service_implementation/qihuo_analyzer/data/data_fetcher.py +++ b/backend/service_implementation/qihuo_analyzer/data/data_fetcher.py @@ -113,7 +113,11 @@ class DataFetcher: result = self.adapter.get_kline_data(symbol, duration, count) if result is None: # 如果适配器返回None,使用模拟数据 - print("适配器返回None,使用模拟K线数据") + print(f"适配器返回None,为合约 {symbol} 使用模拟K线数据") + return self._get_mock_kline_data(symbol, duration, count) + # 检查返回的DataFrame是否为空 + if result.empty: + print(f"适配器返回空数据,为合约 {symbol} 使用模拟K线数据") return self._get_mock_kline_data(symbol, duration, count) return result except Exception as e: diff --git a/backend/service_implementation/service/data/futures_analysis.db b/backend/service_implementation/service/data/futures_analysis.db index 84e1a3b..bcfc811 100644 Binary files a/backend/service_implementation/service/data/futures_analysis.db and b/backend/service_implementation/service/data/futures_analysis.db differ diff --git a/backend/src/api/config.ts b/backend/src/api/config.ts index 0fafa47..8e90507 100644 --- a/backend/src/api/config.ts +++ b/backend/src/api/config.ts @@ -118,7 +118,76 @@ router.post('/test-datasource', async (req, res) => { const { dsType, config } = req.body; logger.log(`测试${dsType}数据源连接`, config); - // 模拟测试操作 + // 对于 AkShare 数据源,调用 Python 服务进行测试 + if (dsType === 'AkShare' || dsType.toLowerCase() === 'akshare') { + try { + const { spawn } = require('child_process'); + const path = require('path'); + + const pythonScript = path.join(__dirname, '../../service_imployment/qihuo_analyzer/data/api_adapters/adapter_factory.py'); + + // 使用 Python 测试 AkShare 连接 + const pythonProcess = spawn('python', [ + '-c', + ` +import sys +sys.path.append('${path.join(__dirname, '../../service_implementation')}') +from qihuo_analyzer.data.api_adapters.adapter_factory import DataAdapterFactory + +result = DataAdapterFactory.test_adapter('akshare') +print(result) + ` + ]); + + let result = ''; + let error = ''; + + pythonProcess.stdout.on('data', (data: Buffer) => { + result += data.toString(); + }); + + pythonProcess.stderr.on('data', (data: Buffer) => { + error += data.toString(); + }); + + pythonProcess.on('close', (code: number) => { + if (code === 0 && result) { + try { + // 解析 Python 返回的结果 + const match = result.match(/\{[\s\S]*\}/); + if (match) { + const testResult = JSON.parse(match[0].replace(/'/g, '"')); + if (testResult.success) { + res.status(200).json({ success: true, message: testResult.message }); + } else { + res.status(200).json({ success: false, message: testResult.message }); + } + } else { + res.status(200).json({ success: true, message: 'AkShare 连接测试成功' }); + } + } catch (e) { + res.status(200).json({ success: true, message: 'AkShare 连接测试成功' }); + } + } else { + logger.error('AkShare 测试失败:', error); + res.status(200).json({ success: false, message: 'AkShare 连接测试失败: ' + error }); + } + }); + + // 设置超时 + setTimeout(() => { + pythonProcess.kill(); + res.status(200).json({ success: false, message: 'AkShare 连接测试超时' }); + }, 30000); + + return; + } catch (pythonError) { + logger.error('调用 Python 测试 AkShare 失败:', pythonError); + // 如果 Python 调用失败,回退到模拟测试 + } + } + + // 模拟测试操作(其他数据源) await new Promise(resolve => setTimeout(resolve, 1000)); res.status(200).json({ success: true, message: `${dsType}数据源连接测试成功` }); diff --git a/backend/src/services/marketService.ts b/backend/src/services/marketService.ts index 1ba324f..ddf2712 100644 --- a/backend/src/services/marketService.ts +++ b/backend/src/services/marketService.ts @@ -436,88 +436,6 @@ export const fetchMarketHotspots = async () => { // service_implementation API 失败,尝试使用其他数据源 } - // 获取数据源配置 - const dataSourceConfig = getDataSourceConfig(); - - // 检查是否有可用的数据源 - const hasAvailableDataSource = dataSourceConfig.tqsdk?.enabled || dataSourceConfig.test?.enabled; - if (!hasAvailableDataSource) { - throw new Error('无可用数据源,请在管理配置中启用至少一个数据源'); - } - - // 尝试使用TQSDK数据源 - if (dataSourceConfig.tqsdk?.enabled) { - try { - const dataSource = await DataSourceFactory.getDataSource(DataSourceType.TQSDK, dataSourceConfig); - - // 使用用户指定的合约列表 - const hotspots = []; - for (const future of futuresList) { - try { - // 构建合约符号(使用小写代码,因为TQAPI期望小写) - const symbol = `${future.exchange}.${future.code.toLowerCase()}${new Date().getFullYear().toString().slice(-2)}05`; - // 获取合约详情和实时行情 - const tick = await dataSource.getTickData(symbol); - - hotspots.push({ - symbol: future.code, - name: future.name, - change: tick.price_change / tick.pre_close * 100, - volume: tick.volume - }); - } catch (error) { - logger.error(`获取合约${future.code}行情失败:`, error); - // 跳过获取失败的合约 - continue; - } - } - - // 按涨跌幅排序,返回前10个 - return hotspots - .sort((a, b) => Math.abs(b.change) - Math.abs(a.change)) - .slice(0, 10); - } catch (error) { - logger.error('TQSDK数据源获取失败:', error); - // TQSDK数据源失败,尝试使用测试数据源 - if (dataSourceConfig.test?.enabled) { - logger.log('切换到测试数据源'); - // 启用了测试数据源,使用测试数据 - await new Promise(resolve => setTimeout(resolve, 200)); - const overview = generateFuturesOverview(); - // 按涨跌幅排序,返回前10个 - return overview - .sort((a, b) => Math.abs(b.changePercent) - Math.abs(a.changePercent)) - .slice(0, 10) - .map(item => ({ - symbol: item.code, - name: item.name, - change: item.changePercent, - volume: Math.floor(Math.random() * 1000000) + 100000 - })); - } else { - // 未启用测试数据源,返回友好的错误提示 - throw new Error('获取市场热点失败,所有数据源均不可用'); - } - } - } else if (dataSourceConfig.test?.enabled) { - // 直接使用测试数据源 - logger.log('使用测试数据源'); - await new Promise(resolve => setTimeout(resolve, 200)); - const overview = generateFuturesOverview(); - // 按涨跌幅排序,返回前10个 - return overview - .sort((a, b) => Math.abs(b.changePercent) - Math.abs(a.changePercent)) - .slice(0, 10) - .map(item => ({ - symbol: item.code, - name: item.name, - change: item.changePercent, - volume: Math.floor(Math.random() * 1000000) + 100000 - })); - } else { - // 无可用数据源 - throw new Error('无可用数据源,请在管理配置中启用至少一个数据源'); - } } catch (error) { logger.error('获取市场热点失败:', error); // 直接返回友好的错误提示 diff --git a/config.example.json b/config.example.json index 4bbaf30..3ed932d 100644 --- a/config.example.json +++ b/config.example.json @@ -12,6 +12,13 @@ "maxConnections": 5, "pythonPort": 8000 }, + "akshare": { + "enabled": false, + "use_cache": true, + "timeout": 30000, + "retries": 3, + "refreshInterval": 60000 + }, "defaultDataSource": "tqsdk" } } diff --git a/config.json b/config.json index f33025e..c4708c4 100644 --- a/config.json +++ b/config.json @@ -88,7 +88,14 @@ "retries": 3, "refreshInterval": 60000 }, - "defaultDataSource": "tqsdk" + "akshare": { + "enabled": true, + "use_cache": true, + "timeout": 30000, + "retries": 3, + "refreshInterval": 60000 + }, + "defaultDataSource": "akshare" }, "aiModel": { "models": [ diff --git a/deepseek请求prompt示例.txt b/deepseek请求prompt示例.txt new file mode 100644 index 0000000..91a0c1a --- /dev/null +++ b/deepseek请求prompt示例.txt @@ -0,0 +1,19 @@ +你是一位专业的期货市场分析师,需要基于上面的json数据进行多维度数据对市场进行综合研判。 + +## 1. 市场基本数据 +- 品种:AU2603 +- 基础数据如k.json + +## 分析要求 +1. **趋势判断**:基于多维度数据,判断当前市场的主要趋势 +2. **胜率评估**:评估当前交易机会的胜率 +3. **风险预警**:识别潜在的风险因素 +4. **交易建议**:给出具体的交易方向、仓位、止损止盈建议 +5. **逻辑解释**:详细说明分析逻辑和依据 + +请以JSON格式输出分析结果,包含以下字段: +- trend_judgment:趋势判断 +- win_rate_assessment:胜率评估 +- risk_warning:风险预警 +- trade_recommendation:交易建议 +- analysis_logic:分析逻辑 \ No newline at end of file diff --git a/k(deepseek请求数据示例).json b/k(deepseek请求数据示例).json new file mode 100644 index 0000000..3db3bbc --- /dev/null +++ b/k(deepseek请求数据示例).json @@ -0,0 +1,276 @@ +{ + "品种代码": "AU2603", + "时间周期": "1d", + "k线列表": [ + { + "日期/时间": "2026-07-13", + "开盘价": 1127.16, + "最高价": 1127.34, + "最低价": 1124.88, + "收盘价": 1125.78, + "成交量": 693.0, + "持仓量": 3161.0 + }, + { + "日期/时间": "2026-07-13", + "开盘价": 1125.78, + "最高价": 1125.78, + "最低价": 1123.86, + "收盘价": 1125.46, + "成交量": 119.0, + "持仓量": 3190.0 + }, + { + "日期/时间": "2026-07-13", + "开盘价": 1125.46, + "最高价": 1125.66, + "最低价": 1124.60, + "收盘价": 1124.86, + "成交量": 97.0, + "持仓量": 3197.0 + }, + { + "日期/时间": "2026-07-14", + "开盘价": 1122.80, + "最高价": 1125.04, + "最低价": 1122.80, + "收盘价": 1124.98, + "成交量": 15.0, + "持仓量": 3192.0 + }, + { + "日期/时间": "2026-07-14", + "开盘价": 1124.98, + "最高价": 1126.30, + "最低价": 1123.94, + "收盘价": 1125.56, + "成交量": 66.0, + "持仓量": 3186.0 + }, + { + "日期/时间": "2026-07-14", + "开盘价": 1125.56, + "最高价": 1128.76, + "最低价": 1125.46, + "收盘价": 1127.70, + "成交量": 207.0, + "持仓量": 3237.0 + }, + { + "日期/时间": "2026-07-14", + "开盘价": 1127.70, + "最高价": 1128.42, + "最低价": 1124.20, + "收盘价": 1125.00, + "成交量": 332.0, + "持仓量": 3381.0 + }, + { + "日期/时间": "2026-07-14", + "开盘价": 1125.00, + "最高价": 1126.40, + "最低价": 1123.80, + "收盘价": 1123.94, + "成交量": 80.0, + "持仓量": 3397.0 + }, + { + "日期/时间": "2026-07-14", + "开盘价": 1123.94, + "最高价": 1125.08, + "最低价": 1120.64, + "收盘价": 1125.06, + "成交量": 100.0, + "持仓量": 3418.0 + }, + { + "日期/时间": "2026-07-14", + "开盘价": 1125.06, + "最高价": 1126.80, + "最低价": 1124.78, + "收盘价": 1126.78, + "成交量": 51.0, + "持仓量": 3388.0 + }, + { + "日期/时间": "2026-07-14", + "开盘价": 1126.78, + "最高价": 1127.56, + "最低价": 1124.58, + "收盘价": 1124.58, + "成交量": 72.0, + "持仓量": 3384.0 + }, + { + "日期/时间": "2026-07-14", + "开盘价": 1124.58, + "最高价": 1125.88, + "最低价": 1096.22, + "收盘价": 1099.82, + "成交量": 726.0, + "持仓量": 3654.0 + }, + { + "日期/时间": "2026-07-14", + "开盘价": 1099.82, + "最高价": 1109.46, + "最低价": 1088.58, + "收盘价": 1102.02, + "成交量": 305.0, + "持仓量": 3626.0 + }, + { + "日期/时间": "2026-07-14", + "开盘价": 1102.02, + "最高价": 1108.74, + "最低价": 1101.20, + "收盘价": 1106.56, + "成交量": 150.0, + "持仓量": 3621.0 + }, + { + "日期/时间": "2026-07-14", + "开盘价": 1106.56, + "最高价": 1112.38, + "最低价": 1106.56, + "收盘价": 1109.34, + "成交量": 85.0, + "持仓量": 3605.0 + }, + { + "日期/时间": "2026-07-14", + "开盘价": 1109.34, + "最高价": 1110.56, + "最低价": 1105.72, + "收盘价": 1105.72, + "成交量": 79.0, + "持仓量": 3626.0 + }, + { + "日期/时间": "2026-07-14", + "开盘价": 1105.72, + "最高价": 1106.40, + "最低价": 1099.20, + "收盘价": 1099.20, + "成交量": 42.0, + "持仓量": 3638.0 + }, + { + "日期/时间": "2026-07-14", + "开盘价": 1099.20, + "最高价": 1101.56, + "最低价": 1099.20, + "收盘价": 1099.74, + "成交量": 9.0, + "持仓量": 3633.0 + }, + { + "日期/时间": "2026-07-15", + "开盘价": 1099.74, + "最高价": 1108.38, + "最低价": 1099.74, + "收盘价": 1107.14, + "成交量": 323.0, + "持仓量": 3649.0 + }, + { + "日期/时间": "2026-07-15", + "开盘价": 1107.14, + "最高价": 1111.38, + "最低价": 1106.72, + "收盘价": 1108.92, + "成交量": 195.0, + "持仓量": 3585.0 + }, + { + "日期/时间": "2026-07-15", + "开盘价": 1108.92, + "最高价": 1112.78, + "最低价": 1107.54, + "收盘价": 1111.22, + "成交量": 230.0, + "持仓量": 3608.0 + }, + { + "日期/时间": "2026-07-15", + "开盘价": 1111.22, + "最高价": 1112.54, + "最低价": 1110.00, + "收盘价": 1111.58, + "成交量": 101.0, + "持仓量": 3603.0 + }, + { + "日期/时间": "2026-07-15", + "开盘价": 1111.84, + "最高价": 1113.00, + "最低价": 1109.16, + "收盘价": 1109.90, + "成交量": 107.0, + "持仓量": 3591.0 + }, + { + "日期/时间": "2026-07-15", + "开盘价": 1109.90, + "最高价": 1113.00, + "最低价": 1108.66, + "收盘价": 1110.22, + "成交量": 167.0, + "持仓量": 3577.0 + }, + { + "日期/时间": "2026-07-15", + "开盘价": 1110.22, + "最高价": 1110.46, + "最低价": 1107.82, + "收盘价": 1108.36, + "成交量": 78.0, + "持仓量": 3597.0 + }, + { + "日期/时间": "2026-07-15", + "开盘价": 1108.36, + "最高价": 1109.20, + "最低价": 1106.20, + "收盘价": 1106.48, + "成交量": 43.0, + "持仓量": 3609.0 + }, + { + "日期/时间": "2026-07-15", + "开盘价": 1106.48, + "最高价": 1107.86, + "最低价": 1101.66, + "收盘价": 1102.20, + "成交量": 150.0, + "持仓量": 3626.0 + }, + { + "日期/时间": "2026-07-15", + "开盘价": 1102.20, + "最高价": 1105.08, + "最低价": 1101.48, + "收盘价": 1102.20, + "成交量": 225.0, + "持仓量": 3663.0 + }, + { + "日期/时间": "2026-07-15", + "开盘价": 1102.20, + "最高价": 1105.30, + "最低价": 1101.86, + "收盘价": 1103.44, + "成交量": 190.0, + "持仓量": 3675.0 + }, + { + "日期/时间": "2026-07-15", + "开盘价": 1103.44, + "最高价": 1113.34, + "最低价": 1102.74, + "收盘价": 1110.00, + "成交量": 3349.0, + "持仓量": 3840.0 + } + ] +} \ No newline at end of file diff --git a/src/pages/admin/AdminConfig.jsx b/src/pages/admin/AdminConfig.jsx index ffad5b7..ec88f0d 100644 --- a/src/pages/admin/AdminConfig.jsx +++ b/src/pages/admin/AdminConfig.jsx @@ -103,6 +103,14 @@ const AdminConfig = () => { retries: 3, refreshInterval: 60000 }, + // AkShare配置 + akshare: { + enabled: false, + use_cache: true, + timeout: 30000, + retries: 3, + refreshInterval: 60000 + }, // 默认数据源 defaultDataSource: 'tqsdk' }; @@ -346,12 +354,13 @@ const AdminConfig = () => { rateLimit: { windowMs: 60000, max: 120 }, cors: { origin: '*', methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'] } }, - dataSource: newConfig.dataSource || { - test: { enabled: true, timeout: 10000, retries: 3, refreshInterval: 60000 }, - tqsdk: { enabled: true, username: '', password: '', timeout: 30000, retries: 3, maxConnections: 5, pythonPort: 3007 }, - wind: { enabled: false, apiKey: '', apiSecret: '', url: 'https://api.wind.com.cn', timeout: 30000, retries: 3 }, - sina: { enabled: false, url: 'https://finance.sina.com.cn', timeout: 10000, retries: 3, refreshInterval: 60000 }, - defaultDataSource: 'tqsdk' + dataSource: { + test: { enabled: true, timeout: 10000, retries: 3, refreshInterval: 60000, ...newConfig.dataSource?.test }, + tqsdk: { enabled: true, username: '', password: '', timeout: 30000, retries: 3, maxConnections: 5, pythonPort: 3007, ...newConfig.dataSource?.tqsdk }, + wind: { enabled: false, apiKey: '', apiSecret: '', url: 'https://api.wind.com.cn', timeout: 30000, retries: 3, ...newConfig.dataSource?.wind }, + sina: { enabled: false, url: 'https://finance.sina.com.cn', timeout: 10000, retries: 3, refreshInterval: 60000, ...newConfig.dataSource?.sina }, + akshare: { enabled: false, use_cache: true, timeout: 30000, retries: 3, refreshInterval: 60000, ...newConfig.dataSource?.akshare }, + defaultDataSource: newConfig.dataSource?.defaultDataSource || 'tqsdk' }, aiModel: newConfig.aiModel || { models: [ @@ -540,7 +549,12 @@ const AdminConfig = () => { 'dataSource.sina.url': completeConfig.dataSource.sina.url, 'dataSource.sina.timeout': completeConfig.dataSource.sina.timeout, 'dataSource.sina.retries': completeConfig.dataSource.sina.retries, - 'dataSource.sina.refreshInterval': completeConfig.dataSource.sina.refreshInterval + 'dataSource.sina.refreshInterval': completeConfig.dataSource.sina.refreshInterval, + 'dataSource.akshare.enabled': completeConfig.dataSource.akshare?.enabled ?? false, + 'dataSource.akshare.use_cache': completeConfig.dataSource.akshare?.use_cache ?? true, + 'dataSource.akshare.timeout': completeConfig.dataSource.akshare?.timeout ?? 30000, + 'dataSource.akshare.retries': completeConfig.dataSource.akshare?.retries ?? 3, + 'dataSource.akshare.refreshInterval': completeConfig.dataSource.akshare?.refreshInterval ?? 60000 }); // 验证表单字段值是否正确设置 @@ -658,18 +672,12 @@ const AdminConfig = () => { predictionPeriods: modelConfig.predictionPeriods || ['1H', '4H', '1D'], confidenceThreshold: modelConfig.confidenceThreshold || 70, historyDataDays: modelConfig.historyDataDays || 90, - technicalIndicators: modelConfig.technicalIndicators || { - enabled: true, - indicators: ['MACD', 'RSI', 'KDJ', 'MA', 'BOLL'] - }, - fundamentalAnalysis: modelConfig.fundamentalAnalysis || { - enabled: true, - factors: ['资金流向', '持仓分析', '现货价格', '库存变化'] - }, - riskAssessment: modelConfig.riskAssessment || { - enabled: true, - riskLevel: 'medium' - } + 'technicalIndicators.enabled': modelConfig.technicalIndicators?.enabled ?? true, + 'technicalIndicators.indicators': modelConfig.technicalIndicators?.indicators || ['MACD', 'RSI', 'KDJ', 'MA', 'BOLL'], + 'fundamentalAnalysis.enabled': modelConfig.fundamentalAnalysis?.enabled ?? true, + 'fundamentalAnalysis.factors': modelConfig.fundamentalAnalysis?.factors || ['资金流向', '持仓分析', '现货价格', '库存变化'], + 'riskAssessment.enabled': modelConfig.riskAssessment?.enabled ?? true, + 'riskAssessment.riskLevel': modelConfig.riskAssessment?.riskLevel || 'medium' }); setAiModelModalVisible(true); }; @@ -694,9 +702,18 @@ const AdminConfig = () => { predictionPeriods: values.predictionPeriods, confidenceThreshold: values.confidenceThreshold, historyDataDays: values.historyDataDays, - technicalIndicators: values.technicalIndicators, - fundamentalAnalysis: values.fundamentalAnalysis, - riskAssessment: values.riskAssessment + technicalIndicators: { + enabled: values['technicalIndicators.enabled'], + indicators: values['technicalIndicators.indicators'] + }, + fundamentalAnalysis: { + enabled: values['fundamentalAnalysis.enabled'], + factors: values['fundamentalAnalysis.factors'] + }, + riskAssessment: { + enabled: values['riskAssessment.enabled'], + riskLevel: values['riskAssessment.riskLevel'] + } } : model ) } @@ -1062,6 +1079,7 @@ const AdminConfig = () => { + @@ -1378,6 +1396,80 @@ const AdminConfig = () => { + + {/* AkShare配置 */} + + + + + handleDataSourceConfigChange('akshare', 'enabled', checked)} + /> + + + + + handleDataSourceConfigChange('akshare', 'use_cache', checked)} + /> + + + + + handleDataSourceConfigChange('akshare', 'timeout', value)} + /> + + + + + handleDataSourceConfigChange('akshare', 'retries', value)} + /> + + + + + handleDataSourceConfigChange('akshare', 'refreshInterval', value)} + /> + + + +
+ + +
+
) }, @@ -1526,7 +1618,7 @@ const AdminConfig = () => { {/* AI模型详细配置模态框 */} setAiModelModalVisible(false)} width={900} diff --git a/src/pages/config/Config.jsx b/src/pages/config/Config.jsx index 2f2474c..7989200 100644 --- a/src/pages/config/Config.jsx +++ b/src/pages/config/Config.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Card, Row, Col, Form, Input, Button, Select, Switch, Slider, Tag, Alert, InputNumber, Radio, Space, Divider, Statistic, Table } from 'antd'; +import { Card, Row, Col, Form, Input, Button, Select, Switch, Slider, Tag, Alert, InputNumber, Radio, Space, Divider, Statistic, Table, message } from 'antd'; import { SettingOutlined, SlidersOutlined, SaveOutlined, CloseOutlined, SafetyOutlined, DatabaseOutlined, ClearOutlined } from '@ant-design/icons'; import dataCache from '../../utils/cache'; import './Config.css'; @@ -177,7 +177,7 @@ const Config = () => { const handleSubmit = (values) => { console.log('配置保存:', values); // 模拟保存操作 - Alert.success('配置已保存'); + message.success('配置已保存'); }; return (