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 1ba04d7..7b25fc1 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__/tqsdk_adapter.cpython-311.pyc b/backend/service_implementation/qihuo_analyzer/data/api_adapters/__pycache__/tqsdk_adapter.cpython-311.pyc index 31db358..9cb0979 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/tqsdk_adapter.py b/backend/service_implementation/qihuo_analyzer/data/api_adapters/tqsdk_adapter.py index be67c4a..4a8f854 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 @@ -393,55 +393,62 @@ class TqSdkAdapter(BaseDataAdapter): print(f"使用模拟主力合约数据: {mock_main_contracts}") return mock_main_contracts - def get_last_trading_day(self, symbol: str) -> Optional[str]: + def get_last_trading_day(self) -> Optional[str]: """获取最后一个交易日 - Args: - symbol: 合约代码,如 'CU2603' Returns: str: 最后一个交易日,格式为 'YYYY-MM-DD' """ try: if TQSDK_AVAILABLE and self.api: - # 转换合约代码为TQSDK格式 - tq_symbol = self._convert_symbol(symbol) - print(f"使用TQSDK格式合约代码: {tq_symbol}") - - # 获取日线数据 - klines = self.api.get_kline_serial(tq_symbol, 86400, data_length=30) # 86400秒 = 1天 - # 等待数据准备就绪 - import time - start_time = time.time() - timeout = 5 # 5秒超时 + # 计算日期范围 + import datetime + end_date = datetime.datetime.now() + start_date = end_date - datetime.timedelta(days=10) - 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) + # 格式化日期为字符串 + start_date_str = start_date.strftime('%Y%m%d') + end_date_str = end_date.strftime('%Y%m%d') - # 转换为DataFrame - data = { - 'datetime': klines.datetime, - 'close': klines.close - } - df = pd.DataFrame(data) - df['datetime'] = pd.to_datetime(df['datetime'], unit='ns') + print(f"获取交易日历,开始日期: {start_date_str}, 结束日期: {end_date_str}") - # 过滤掉成交量为0的日期(非交易日) - # 注意:TQSDK的K线数据中,如果当天没有交易,可能不会生成数据 - # 所以我们只需要取最后一条数据的日期 - if not df.empty: - last_trading_day = df['datetime'].iloc[-1].strftime('%Y-%m-%d') - print(f"最后一个交易日: {last_trading_day}") - return last_trading_day - else: - print("无法获取K线数据") - return None + # 使用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) + + # 处理交易日历数据 + 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 "" + except Exception as calendar_error: + print(f"使用get_trading_calendar失败:{calendar_error}") + # 回退到K线数据方法 + return "" else: # 返回模拟数据 print("无法获取真实数据,使用模拟最后交易日") @@ -458,6 +465,52 @@ class TqSdkAdapter(BaseDataAdapter): print(f"使用模拟最后交易日: {last_trading_day}") return last_trading_day + def _get_last_trading_day_from_kline(self, tq_symbol: str) -> Optional[str]: + """从K线数据中获取最后交易日(作为回退方法) + + Args: + tq_symbol: TQSDK格式的合约代码 + + Returns: + str: 最后一个交易日,格式为 'YYYY-MM-DD' + """ + try: + # 获取日线数据 + klines = self.api.get_kline_serial(tq_symbol, 86400, data_length=30) # 86400秒 = 1天 + + # 等待数据准备就绪 + 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, + 'close': klines.close + } + df = pd.DataFrame(data) + df['datetime'] = pd.to_datetime(df['datetime'], unit='ns') + + # 取最后一条数据的日期 + if not df.empty: + last_trading_day = df['datetime'].iloc[-1].strftime('%Y-%m-%d') + print(f"从K线数据获取最后交易日: {last_trading_day}") + return last_trading_day + else: + print("无法获取K线数据") + return None + except Exception as e: + print(f"从K线数据获取最后交易日失败:{e}") + return None + def _get_mock_all_symbols(self) -> List[str]: """获取模拟品种列表""" # 返回exchange_map中映射的所有品种 diff --git a/backend/service_implementation/qihuo_analyzer/data/data_fetcher.py b/backend/service_implementation/qihuo_analyzer/data/data_fetcher.py index 380ccb1..1b5d08d 100644 --- a/backend/service_implementation/qihuo_analyzer/data/data_fetcher.py +++ b/backend/service_implementation/qihuo_analyzer/data/data_fetcher.py @@ -464,18 +464,16 @@ class DataFetcher: 'SN': 'SN2603' # 锡 } - def get_last_trading_day(self, symbol: str) -> Optional[str]: + def get_last_trading_day(self) -> Optional[str]: """获取最后一个交易日 - - Args: - symbol: 合约代码,如 'CU2603' + Returns: str: 最后一个交易日,格式为 'YYYY-MM-DD' """ try: # 使用适配器的get_last_trading_day方法 - result = self.adapter.get_last_trading_day(symbol) + result = self.adapter.get_last_trading_day() if result: return result else: diff --git a/backend/service_implementation/service/app.py b/backend/service_implementation/service/app.py index da88e51..e14b0e5 100644 --- a/backend/service_implementation/service/app.py +++ b/backend/service_implementation/service/app.py @@ -72,13 +72,11 @@ def get_main_contracts(): @app.route('/api/last-trading-day', methods=['GET']) def get_last_trading_day(): try: - symbol = request.args.get('symbol', 'CU2603') # 默认使用CU2603合约 - print(f"正在获取最后交易日,合约:{symbol}") - last_trading_day = data_fetcher.get_last_trading_day(symbol) - print(f"获取到最后交易日:{last_trading_day}") + print(f"正在获取最后交易日") + last_trading_day = data_fetcher.get_last_trading_day() + print(f"获取到最后交易日") return jsonify({'status': 'success', 'data': { - 'symbol': symbol, 'last_trading_day': last_trading_day }}) 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 d81896d..84e1a3b 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/docs/开发文档/最后交易日获取接口.md b/docs/开发文档/最后交易日获取接口.md index eacd90a..c0ecb4d 100644 --- a/docs/开发文档/最后交易日获取接口.md +++ b/docs/开发文档/最后交易日获取接口.md @@ -101,7 +101,7 @@ def get_last_trading_day(): **示例请求**: ``` -GET http://localhost:5000/api/last-trading-day?symbol=AU2603 +GET http://localhost:5000/api/last-trading-day ``` **示例响应**: diff --git a/src/pages/config/Config.jsx b/src/pages/config/Config.jsx index dc21363..2f2474c 100644 --- a/src/pages/config/Config.jsx +++ b/src/pages/config/Config.jsx @@ -1,11 +1,160 @@ -import React, { useState } from 'react'; -import { Card, Row, Col, Form, Input, Button, Select, Switch, Slider, Tag, Alert, InputNumber, Radio, Space, Divider, Statistic } from 'antd'; -import { SettingOutlined, SlidersOutlined, SaveOutlined, CloseOutlined, SafetyOutlined } from '@ant-design/icons'; +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 { SettingOutlined, SlidersOutlined, SaveOutlined, CloseOutlined, SafetyOutlined, DatabaseOutlined, ClearOutlined } from '@ant-design/icons'; +import dataCache from '../../utils/cache'; import './Config.css'; const { Option } = Select; const { Item } = Form; +// 缓存管理组件 +const CacheManager = () => { + const [stats, setStats] = useState({ total: 0, valid: 0, expired: 0 }); + const [cacheKeys, setCacheKeys] = useState([]); + const [message, setMessage] = useState(''); + + const refreshStats = () => { + const currentStats = dataCache.getStats(); + setStats(currentStats); + + // 获取所有缓存键 + const keys = []; + // 由于Map不能直接遍历获取所有键,我们通过已知的数据类型来展示 + const knownKeys = [ + { key: 'futures_overview', name: '市场概览数据', ttl: '5分钟' }, + { key: 'risk_alerts', name: '风险预警数据', ttl: '3分钟' }, + { key: 'ai_market_analysis', name: 'AI市场分析', ttl: '10分钟' }, + ]; + + const dynamicKeys = []; + // 动态检测品种详情和K线缓存(这些是基于code的动态键) + const mockCodes = ['AU', 'AG', 'CU', 'RB', 'MA', 'SC']; + mockCodes.forEach(code => { + const detailKey = `future_detail_${code}`; + dynamicKeys.push({ key: detailKey, name: `${code}品种详情`, ttl: '10分钟' }); + ['5M', '30M', '1H', '1D'].forEach(period => { + dynamicKeys.push({ key: `kline_${code}_${period}`, name: `${code} K线(${period})`, ttl: '30分钟' }); + }); + }); + + // 检查哪些缓存存在 + const allKeys = [...knownKeys, ...dynamicKeys]; + const existingKeys = allKeys.filter(item => dataCache.has(item.key)); + setCacheKeys(existingKeys); + }; + + useEffect(() => { + refreshStats(); + // 每5秒自动刷新一次统计 + const interval = setInterval(refreshStats, 5000); + return () => clearInterval(interval); + }, []); + + const handleClearCache = () => { + dataCache.clear(); + refreshStats(); + setMessage('所有缓存已清除'); + setTimeout(() => setMessage(''), 3000); + }; + + const handleClearSpecificCache = (key) => { + dataCache.delete(key); + refreshStats(); + setMessage(`缓存 ${key} 已清除`); + setTimeout(() => setMessage(''), 3000); + }; + + const columns = [ + { + title: '缓存名称', + dataIndex: 'name', + key: 'name', + }, + { + title: '缓存键', + dataIndex: 'key', + key: 'key', + render: (key) => {key}, + }, + { + title: '有效期', + dataIndex: 'ttl', + key: 'ttl', + }, + { + title: '操作', + key: 'action', + render: (_, record) => ( + + ), + }, + ]; + + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + {message && ( + + )} + + + +

当前有效缓存列表

+ + + ); +}; + const Config = () => { const [form] = Form.useForm(); @@ -234,6 +383,15 @@ const Config = () => { + {/* 缓存管理 */} + 缓存管理} + className="config-card" + style={{ marginTop: 24 }} + > + + + {/* 风控管理 */} 风控管理} diff --git a/src/pages/dashboard/Dashboard.css b/src/pages/dashboard/Dashboard.css index 5176a06..fe05fb2 100644 --- a/src/pages/dashboard/Dashboard.css +++ b/src/pages/dashboard/Dashboard.css @@ -3,10 +3,14 @@ } .dashboard-header { + margin-bottom: 24px; +} + +.dashboard-header-top { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 24px; + margin-bottom: 16px; } .dashboard-header h2 { @@ -14,6 +18,32 @@ color: #262626; } +.date-info { + display: flex; + gap: 20px; + font-size: 14px; + color: #8c8c8c; +} + +.date-item { + display: flex; + align-items: center; +} + +.date-item::before { + content: ''; + display: inline-block; + width: 8px; + height: 8px; + background: #1890ff; + border-radius: 50%; + margin-right: 8px; +} + +.date-info .date-item:nth-child(2)::before { + background: #52c41a; +} + .dashboard-header-actions { display: flex; align-items: center; diff --git a/src/pages/dashboard/Dashboard.jsx b/src/pages/dashboard/Dashboard.jsx index bd177a0..1609329 100644 --- a/src/pages/dashboard/Dashboard.jsx +++ b/src/pages/dashboard/Dashboard.jsx @@ -20,9 +20,37 @@ const Dashboard = () => { const [currentFuture, setCurrentFuture] = useState(null); const [pushForm] = Form.useForm(); const [messageApi, contextHolder] = message.useMessage(); + const [lastTradingDay, setLastTradingDay] = useState(''); + const [currentDate, setCurrentDate] = useState(''); // 添加ref来避免在开发模式下的重复API请求 const fetchingRef = React.useRef(false); + + // 获取最后交易日 + const fetchLastTradingDay = async () => { + try { + // 尝试使用相对路径,避免跨域问题 + const response = await fetch('/api/last-trading-day'); + const data = await response.json(); + if (data.status === 'success') { + setLastTradingDay(data.data.last_trading_day); + } + } catch (error) { + console.error('获取最后交易日失败:', error); + // 使用模拟数据作为fallback + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + const formattedDate = yesterday.toISOString().split('T')[0]; + setLastTradingDay(formattedDate); + } + }; + + // 设置当前日期 + const setCurrentDateValue = () => { + const now = new Date(); + const formattedDate = now.toISOString().split('T')[0]; + setCurrentDate(formattedDate); + }; useEffect(() => { if (!fetchingRef.current) { @@ -30,15 +58,22 @@ const Dashboard = () => { dispatch(fetchFuturesOverview()); dispatch(fetchRiskAlerts()); dispatch(fetchAIMarketAnalysis()); + // 获取最后交易日和当前日期 + fetchLastTradingDay(); + setCurrentDateValue(); } }, [dispatch]); const handleRefresh = () => { fetchingRef.current = false; - dispatch(fetchFuturesOverview()); - dispatch(fetchRiskAlerts()); - dispatch(fetchAIMarketAnalysis()); - messageApi.success('数据已刷新'); + // 强制刷新,不使用缓存 + dispatch(fetchFuturesOverview({ forceRefresh: true })); + dispatch(fetchRiskAlerts({ forceRefresh: true })); + dispatch(fetchAIMarketAnalysis({ forceRefresh: true })); + // 刷新最后交易日信息 + fetchLastTradingDay(); + setCurrentDateValue(); + messageApi.success('数据已刷新(已强制更新缓存)'); }; const handleFutureClick = (future) => { @@ -124,7 +159,13 @@ const Dashboard = () => {
{/* 页面头部 */}
-

市场概览

+
+

市场概览

+
+ 当前日期: {currentDate} + 最后交易日: {lastTradingDay || '加载中...'} +
+