fix: 增加akshare的接口

master
Lxy 3 months ago
parent 229c10a225
commit 0896f52f59

@ -4,6 +4,7 @@ import os
from qihuo_analyzer.data.api_adapters.base_adapter import BaseDataAdapter 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.tqsdk_adapter import TqSdkAdapter
from qihuo_analyzer.data.api_adapters.rqdata_adapter import RqDataAdapter from qihuo_analyzer.data.api_adapters.rqdata_adapter import RqDataAdapter
from qihuo_analyzer.data.api_adapters.akshare_adapter import AkShareAdapter
class DataAdapterFactory: class DataAdapterFactory:
@ -38,7 +39,7 @@ class DataAdapterFactory:
"""创建数据适配器 """创建数据适配器
Args: Args:
adapter_type: 适配器类型可选值'tqsdk', 'rqdata'如果为None则从配置文件获取 adapter_type: 适配器类型可选值'tqsdk', 'rqdata', 'akshare'如果为None则从配置文件获取
Returns: Returns:
BaseDataAdapter: 数据适配器实例 BaseDataAdapter: 数据适配器实例
@ -55,9 +56,31 @@ class DataAdapterFactory:
# 根据类型创建适配器 # 根据类型创建适配器
if adapter_type == 'tqsdk': 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数据适配器") print("创建TQSDK数据适配器")
# 获取TQSDK配置 # 获取TQSDK配置
tqsdk_config = data_source_config.get('tqsdk', {})
username = tqsdk_config.get('username', '') username = tqsdk_config.get('username', '')
password = tqsdk_config.get('password', '') password = tqsdk_config.get('password', '')
return TqSdkAdapter(username=username, password=password) return TqSdkAdapter(username=username, password=password)
@ -68,6 +91,12 @@ class DataAdapterFactory:
username = rqdata_config.get('username', '') username = rqdata_config.get('username', '')
password = rqdata_config.get('password', '') password = rqdata_config.get('password', '')
return RqDataAdapter(username=username, password=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: else:
# 默认使用TQSDK适配器 # 默认使用TQSDK适配器
print(f"未知的适配器类型:{adapter_type}使用默认的TQSDK适配器") print(f"未知的适配器类型:{adapter_type}使用默认的TQSDK适配器")
@ -76,3 +105,48 @@ class DataAdapterFactory:
username = tqsdk_config.get('username', '') username = tqsdk_config.get('username', '')
password = tqsdk_config.get('password', '') password = tqsdk_config.get('password', '')
return TqSdkAdapter(username=username, password=password) 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)}'
}

@ -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

@ -5,14 +5,23 @@ import pandas as pd
from typing import Dict, Optional, List from typing import Dict, Optional, List
from qihuo_analyzer.data.api_adapters.base_adapter import BaseDataAdapter from qihuo_analyzer.data.api_adapters.base_adapter import BaseDataAdapter
# 尝试导入tqsdk # 尝试导入tqsdk仅当需要时
TQSDK_AVAILABLE = False
try:
# 延迟导入,仅在需要时导入
def _try_import_tqsdk():
"""尝试导入tqsdk"""
global TQSDK_AVAILABLE
if not TQSDK_AVAILABLE:
try:
from tqsdk import TqApi, TqAuth from tqsdk import TqApi, TqAuth
TQSDK_AVAILABLE = True TQSDK_AVAILABLE = True
except Exception as e: return True
except Exception as e:
print(f"tqsdk导入失败{e},将使用模拟数据") print(f"tqsdk导入失败{e},将使用模拟数据")
TQSDK_AVAILABLE = False TQSDK_AVAILABLE = False
return False
return True
class TqSdkAdapter(BaseDataAdapter): class TqSdkAdapter(BaseDataAdapter):
@ -41,12 +50,20 @@ class TqSdkAdapter(BaseDataAdapter):
bool: 连接是否成功 bool: 连接是否成功
""" """
try: try:
# 尝试导入tqsdk
if not _try_import_tqsdk():
# TQSDK不可用
print("TQSDK不可用使用模拟API")
self.api = None
return False
if TQSDK_AVAILABLE: if TQSDK_AVAILABLE:
# 使用天勤TQSDK连接 # 使用天勤TQSDK连接
username = self.username or os.getenv('TQSDK_USERNAME', '') username = self.username or os.getenv('TQSDK_USERNAME', '')
password = self.password or os.getenv('TQSDK_PASSWORD', '') password = self.password or os.getenv('TQSDK_PASSWORD', '')
if username and password: if username and password:
from tqsdk import TqApi, TqAuth
self.api = TqApi(auth=TqAuth(username, password)) self.api = TqApi(auth=TqAuth(username, password))
print("TQSDK API连接成功") print("TQSDK API连接成功")
return True return True
@ -148,7 +165,11 @@ class TqSdkAdapter(BaseDataAdapter):
K线数据DataFrame如果无法获取真实数据则返回None K线数据DataFrame如果无法获取真实数据则返回None
""" """
try: try:
if TQSDK_AVAILABLE and self.api: # 检查TQSDK是否可用且已连接
if not TQSDK_AVAILABLE or not self.api:
print(f"无法获取真实数据:{'API未连接' if not self.api else 'TQSDK不可用'}")
return None
# 转换合约代码为TQSDK格式 # 转换合约代码为TQSDK格式
tq_symbol = self._convert_symbol(symbol) tq_symbol = self._convert_symbol(symbol)
print(f"使用TQSDK格式合约代码: {tq_symbol}") print(f"使用TQSDK格式合约代码: {tq_symbol}")
@ -171,7 +192,18 @@ class TqSdkAdapter(BaseDataAdapter):
return None return None
time.sleep(0.1) 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 # 转换为DataFrame
try:
data = { data = {
'datetime': klines.datetime, 'datetime': klines.datetime,
'open': klines.open, 'open': klines.open,
@ -187,9 +219,8 @@ class TqSdkAdapter(BaseDataAdapter):
print(f"成功获取K线数据数据长度: {len(df)}") print(f"成功获取K线数据数据长度: {len(df)}")
return df return df
else: except Exception as e:
# 不再自动返回模拟数据返回None print(f"创建DataFrame失败{e}")
print(f"无法获取真实数据:{'API未连接' if not self.api else 'TQSDK不可用'}")
return None return None
except Exception as e: except Exception as e:
print(f"tqsdk获取K线数据失败{e}") print(f"tqsdk获取K线数据失败{e}")
@ -199,7 +230,11 @@ class TqSdkAdapter(BaseDataAdapter):
def get_tick_data(self, symbol: str, count: int = 1000) -> Optional[pd.DataFrame]: def get_tick_data(self, symbol: str, count: int = 1000) -> Optional[pd.DataFrame]:
"""获取Tick数据""" """获取Tick数据"""
try: try:
if TQSDK_AVAILABLE and self.api: # 检查TQSDK是否可用且已连接
if not TQSDK_AVAILABLE or not self.api:
print(f"无法获取真实数据:{'API未连接' if not self.api else 'TQSDK不可用'}")
return None
# 使用真实API获取数据 # 使用真实API获取数据
ticks = self.api.get_tick_serial(symbol, data_length=count) ticks = self.api.get_tick_serial(symbol, data_length=count)
self.api.wait_update() self.api.wait_update()
@ -220,10 +255,6 @@ class TqSdkAdapter(BaseDataAdapter):
df.set_index('datetime', inplace=True) df.set_index('datetime', inplace=True)
return df return df
else:
# 返回模拟数据
print(f"无法获取真实数据:{'API未连接' if not self.api else 'TQSDK不可用'}")
return None
except Exception as e: except Exception as e:
print(f"获取Tick数据失败{e}") print(f"获取Tick数据失败{e}")
return None return None
@ -231,7 +262,11 @@ class TqSdkAdapter(BaseDataAdapter):
def get_contract_info(self, symbol: str) -> Optional[Dict]: def get_contract_info(self, symbol: str) -> Optional[Dict]:
"""获取合约信息""" """获取合约信息"""
try: try:
if TQSDK_AVAILABLE and self.api: # 检查TQSDK是否可用且已连接
if not TQSDK_AVAILABLE or not self.api:
print(f"无法获取真实数据:{'API未连接' if not self.api else 'TQSDK不可用'}")
return None
# 使用真实API获取数据 # 使用真实API获取数据
quote = self.api.get_quote(symbol) quote = self.api.get_quote(symbol)
self.api.wait_update() self.api.wait_update()
@ -247,10 +282,6 @@ class TqSdkAdapter(BaseDataAdapter):
'expire_datetime': quote.expire_datetime, 'expire_datetime': quote.expire_datetime,
'create_datetime': quote.create_datetime 'create_datetime': quote.create_datetime
} }
else:
# 返回模拟数据
print(f"无法获取真实数据:{'API未连接' if not self.api else 'TQSDK不可用'}")
return None
except Exception as e: except Exception as e:
print(f"获取合约信息失败:{e}") print(f"获取合约信息失败:{e}")
return None return None
@ -259,9 +290,14 @@ class TqSdkAdapter(BaseDataAdapter):
"""批量获取市场数据""" """批量获取市场数据"""
market_data = {} 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: for symbol in symbols:
try: try:
if TQSDK_AVAILABLE and self.api:
quote = self.api.get_quote(symbol) quote = self.api.get_quote(symbol)
self.api.wait_update() self.api.wait_update()
@ -276,19 +312,6 @@ class TqSdkAdapter(BaseDataAdapter):
'bid_price1': quote.bid_price1, 'bid_price1': quote.bid_price1,
'ask_price1': quote.ask_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: except Exception as e:
print(f"获取{symbol}市场数据失败:{e}") print(f"获取{symbol}市场数据失败:{e}")
market_data[symbol] = { market_data[symbol] = {
@ -331,7 +354,11 @@ class TqSdkAdapter(BaseDataAdapter):
Dict[str, str]: 品种代码到主力合约代码的映射 Dict[str, str]: 品种代码到主力合约代码的映射
""" """
try: try:
if TQSDK_AVAILABLE and self.api: # 检查TQSDK是否可用且已连接
if not TQSDK_AVAILABLE or not self.api:
print("无法获取真实主力合约数据,使用模拟数据")
return self._get_mock_main_contracts()
# 使用TQSDK的query_quotes方法获取主力合约 # 使用TQSDK的query_quotes方法获取主力合约
main_contracts = {} main_contracts = {}
@ -372,10 +399,6 @@ class TqSdkAdapter(BaseDataAdapter):
print(f"获取到主力合约:{main_contracts}") print(f"获取到主力合约:{main_contracts}")
return main_contracts return main_contracts
else:
# 返回模拟数据
print("无法获取真实主力合约数据,使用模拟数据")
return self._get_mock_main_contracts()
except Exception as e: except Exception as e:
print(f"获取主力合约失败:{e}") print(f"获取主力合约失败:{e}")
return self._get_mock_main_contracts() return self._get_mock_main_contracts()
@ -401,7 +424,15 @@ class TqSdkAdapter(BaseDataAdapter):
str: 最后一个交易日格式为 'YYYY-MM-DD' str: 最后一个交易日格式为 'YYYY-MM-DD'
""" """
try: try:
if TQSDK_AVAILABLE and self.api: # 检查TQSDK是否可用且已连接
if not TQSDK_AVAILABLE or not self.api:
# 返回模拟数据
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
# 计算日期范围 # 计算日期范围
import datetime import datetime
@ -449,14 +480,6 @@ class TqSdkAdapter(BaseDataAdapter):
print(f"使用get_trading_calendar失败{calendar_error}") print(f"使用get_trading_calendar失败{calendar_error}")
# 回退到K线数据方法 # 回退到K线数据方法
return "" 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 e: except Exception as e:
print(f"获取最后交易日失败:{e}") print(f"获取最后交易日失败:{e}")
# 返回模拟数据 # 返回模拟数据

@ -113,7 +113,11 @@ class DataFetcher:
result = self.adapter.get_kline_data(symbol, duration, count) result = self.adapter.get_kline_data(symbol, duration, count)
if result is None: if result is None:
# 如果适配器返回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 self._get_mock_kline_data(symbol, duration, count)
return result return result
except Exception as e: except Exception as e:

@ -118,7 +118,76 @@ router.post('/test-datasource', async (req, res) => {
const { dsType, config } = req.body; const { dsType, config } = req.body;
logger.log(`测试${dsType}数据源连接`, config); 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)); await new Promise(resolve => setTimeout(resolve, 1000));
res.status(200).json({ success: true, message: `${dsType}数据源连接测试成功` }); res.status(200).json({ success: true, message: `${dsType}数据源连接测试成功` });

@ -436,88 +436,6 @@ export const fetchMarketHotspots = async () => {
// service_implementation API 失败,尝试使用其他数据源 // 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) { } catch (error) {
logger.error('获取市场热点失败:', error); logger.error('获取市场热点失败:', error);
// 直接返回友好的错误提示 // 直接返回友好的错误提示

@ -12,6 +12,13 @@
"maxConnections": 5, "maxConnections": 5,
"pythonPort": 8000 "pythonPort": 8000
}, },
"akshare": {
"enabled": false,
"use_cache": true,
"timeout": 30000,
"retries": 3,
"refreshInterval": 60000
},
"defaultDataSource": "tqsdk" "defaultDataSource": "tqsdk"
} }
} }

@ -88,7 +88,14 @@
"retries": 3, "retries": 3,
"refreshInterval": 60000 "refreshInterval": 60000
}, },
"defaultDataSource": "tqsdk" "akshare": {
"enabled": true,
"use_cache": true,
"timeout": 30000,
"retries": 3,
"refreshInterval": 60000
},
"defaultDataSource": "akshare"
}, },
"aiModel": { "aiModel": {
"models": [ "models": [

@ -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分析逻辑

@ -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
}
]
}

@ -103,6 +103,14 @@ const AdminConfig = () => {
retries: 3, retries: 3,
refreshInterval: 60000 refreshInterval: 60000
}, },
// AkShare
akshare: {
enabled: false,
use_cache: true,
timeout: 30000,
retries: 3,
refreshInterval: 60000
},
// //
defaultDataSource: 'tqsdk' defaultDataSource: 'tqsdk'
}; };
@ -346,12 +354,13 @@ const AdminConfig = () => {
rateLimit: { windowMs: 60000, max: 120 }, rateLimit: { windowMs: 60000, max: 120 },
cors: { origin: '*', methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'] } cors: { origin: '*', methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'] }
}, },
dataSource: newConfig.dataSource || { dataSource: {
test: { enabled: true, timeout: 10000, retries: 3, refreshInterval: 60000 }, 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 }, 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 }, 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 }, sina: { enabled: false, url: 'https://finance.sina.com.cn', timeout: 10000, retries: 3, refreshInterval: 60000, ...newConfig.dataSource?.sina },
defaultDataSource: 'tqsdk' akshare: { enabled: false, use_cache: true, timeout: 30000, retries: 3, refreshInterval: 60000, ...newConfig.dataSource?.akshare },
defaultDataSource: newConfig.dataSource?.defaultDataSource || 'tqsdk'
}, },
aiModel: newConfig.aiModel || { aiModel: newConfig.aiModel || {
models: [ models: [
@ -540,7 +549,12 @@ const AdminConfig = () => {
'dataSource.sina.url': completeConfig.dataSource.sina.url, 'dataSource.sina.url': completeConfig.dataSource.sina.url,
'dataSource.sina.timeout': completeConfig.dataSource.sina.timeout, 'dataSource.sina.timeout': completeConfig.dataSource.sina.timeout,
'dataSource.sina.retries': completeConfig.dataSource.sina.retries, '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'], predictionPeriods: modelConfig.predictionPeriods || ['1H', '4H', '1D'],
confidenceThreshold: modelConfig.confidenceThreshold || 70, confidenceThreshold: modelConfig.confidenceThreshold || 70,
historyDataDays: modelConfig.historyDataDays || 90, historyDataDays: modelConfig.historyDataDays || 90,
technicalIndicators: modelConfig.technicalIndicators || { 'technicalIndicators.enabled': modelConfig.technicalIndicators?.enabled ?? true,
enabled: true, 'technicalIndicators.indicators': modelConfig.technicalIndicators?.indicators || ['MACD', 'RSI', 'KDJ', 'MA', 'BOLL'],
indicators: ['MACD', 'RSI', 'KDJ', 'MA', 'BOLL'] 'fundamentalAnalysis.enabled': modelConfig.fundamentalAnalysis?.enabled ?? true,
}, 'fundamentalAnalysis.factors': modelConfig.fundamentalAnalysis?.factors || ['资金流向', '持仓分析', '现货价格', '库存变化'],
fundamentalAnalysis: modelConfig.fundamentalAnalysis || { 'riskAssessment.enabled': modelConfig.riskAssessment?.enabled ?? true,
enabled: true, 'riskAssessment.riskLevel': modelConfig.riskAssessment?.riskLevel || 'medium'
factors: ['资金流向', '持仓分析', '现货价格', '库存变化']
},
riskAssessment: modelConfig.riskAssessment || {
enabled: true,
riskLevel: 'medium'
}
}); });
setAiModelModalVisible(true); setAiModelModalVisible(true);
}; };
@ -694,9 +702,18 @@ const AdminConfig = () => {
predictionPeriods: values.predictionPeriods, predictionPeriods: values.predictionPeriods,
confidenceThreshold: values.confidenceThreshold, confidenceThreshold: values.confidenceThreshold,
historyDataDays: values.historyDataDays, historyDataDays: values.historyDataDays,
technicalIndicators: values.technicalIndicators, technicalIndicators: {
fundamentalAnalysis: values.fundamentalAnalysis, enabled: values['technicalIndicators.enabled'],
riskAssessment: values.riskAssessment indicators: values['technicalIndicators.indicators']
},
fundamentalAnalysis: {
enabled: values['fundamentalAnalysis.enabled'],
factors: values['fundamentalAnalysis.factors']
},
riskAssessment: {
enabled: values['riskAssessment.enabled'],
riskLevel: values['riskAssessment.riskLevel']
}
} : model } : model
) )
} }
@ -1062,6 +1079,7 @@ const AdminConfig = () => {
<Option value="tqsdk">TQSDK</Option> <Option value="tqsdk">TQSDK</Option>
<Option value="wind">Wind</Option> <Option value="wind">Wind</Option>
<Option value="sina">新浪财经</Option> <Option value="sina">新浪财经</Option>
<Option value="akshare">AkShare</Option>
</Select> </Select>
</Item> </Item>
</Col> </Col>
@ -1378,6 +1396,80 @@ const AdminConfig = () => {
</Button> </Button>
</div> </div>
</Card> </Card>
{/* AkShare配置 */}
<Card title="AkShare配置" className="admin-config-card" style={{ marginBottom: 24 }}>
<Row gutter={[16, 16]}>
<Col span={6}>
<Item label="启用" name="dataSource.akshare.enabled">
<Switch
onChange={(checked) => handleDataSourceConfigChange('akshare', 'enabled', checked)}
/>
</Item>
</Col>
<Col span={6}>
<Item label="使用缓存" name="dataSource.akshare.use_cache">
<Switch
onChange={(checked) => handleDataSourceConfigChange('akshare', 'use_cache', checked)}
/>
</Item>
</Col>
<Col span={6}>
<Item label="超时时间(ms)" name="dataSource.akshare.timeout">
<InputNumber
min={1000}
max={60000}
step={1000}
onChange={(value) => handleDataSourceConfigChange('akshare', 'timeout', value)}
/>
</Item>
</Col>
<Col span={6}>
<Item label="重试次数" name="dataSource.akshare.retries">
<InputNumber
min={0}
max={10}
step={1}
onChange={(value) => handleDataSourceConfigChange('akshare', 'retries', value)}
/>
</Item>
</Col>
<Col span={6}>
<Item label="刷新间隔(ms)" name="dataSource.akshare.refreshInterval">
<InputNumber
min={1000}
max={300000}
step={1000}
onChange={(value) => handleDataSourceConfigChange('akshare', 'refreshInterval', value)}
/>
</Item>
</Col>
</Row>
<div style={{ marginTop: 16 }}>
<Button
type="primary"
onClick={() => testDataSourceConnection('AkShare')}
style={{ marginRight: 8 }}
>
测试连接
</Button>
<Button
type="default"
onClick={() => {
handleDataSourceConfigChange('akshare', 'enabled', false);
handleDataSourceConfigChange('akshare', 'use_cache', true);
handleDataSourceConfigChange('akshare', 'timeout', 30000);
handleDataSourceConfigChange('akshare', 'retries', 3);
handleDataSourceConfigChange('akshare', 'refreshInterval', 60000);
}}
>
恢复默认
</Button>
</div>
</Card>
</> </>
) )
}, },
@ -1526,7 +1618,7 @@ const AdminConfig = () => {
{/* AI模型详细配置模态框 */} {/* AI模型详细配置模态框 */}
<Modal <Modal
title={`${currentAiModel?.name || 'AI模型'}详细配置`} title={currentAiModel ? `${currentAiModel.name}详细配置` : 'AI模型详细配置'}
open={aiModelModalVisible} open={aiModelModalVisible}
onCancel={() => setAiModelModalVisible(false)} onCancel={() => setAiModelModalVisible(false)}
width={900} width={900}

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; 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 { SettingOutlined, SlidersOutlined, SaveOutlined, CloseOutlined, SafetyOutlined, DatabaseOutlined, ClearOutlined } from '@ant-design/icons';
import dataCache from '../../utils/cache'; import dataCache from '../../utils/cache';
import './Config.css'; import './Config.css';
@ -177,7 +177,7 @@ const Config = () => {
const handleSubmit = (values) => { const handleSubmit = (values) => {
console.log('配置保存:', values); console.log('配置保存:', values);
// //
Alert.success('配置已保存'); message.success('配置已保存');
}; };
return ( return (

Loading…
Cancel
Save