diff --git a/backend/app/api/v1/cache.py b/backend/app/api/v1/cache.py index af59709..0597428 100644 --- a/backend/app/api/v1/cache.py +++ b/backend/app/api/v1/cache.py @@ -395,3 +395,109 @@ async def get_main_contracts( from app.utils.date_utils import format_date + + +@router.get("/missing-dates/{code}", response_model=ResponseModel) +async def get_missing_dates_for_code( + code: str, + security_type: str = Query("stock", description="证券类型: stock, future"), + period_type: str = Query("daily", description="周期类型: daily, min1, min5"), + start_date: str = Query(..., description="开始日期 YYYYMMDD"), + end_date: str = Query(..., description="结束日期 YYYYMMDD"), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """获取单个代码的缺失交易日详情""" + service = CacheService(db) + start = parse_date(start_date) + end = parse_date(end_date) + + result = service.get_missing_dates_for_code(code, security_type, period_type, start, end) + + return ResponseModel(data=result) + + +@router.post("/fill-single-date", response_model=ResponseModel) +async def fill_single_date_data( + request: dict, + background_tasks: BackgroundTasks, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """补齐单个代码的单个交易日数据""" + service = CacheService(db) + + code = request.get("code") + trade_date = request.get("trade_date") + security_type = request.get("security_type", "stock") + period_type = request.get("period_type", "daily") + + if not code or not trade_date: + return ResponseModel(code=400, message="缺少必要参数: code 或 trade_date") + + from app.utils.date_utils import parse_date + date_obj = parse_date(trade_date) + + def run_fill_task(): + from app.db.session import SessionLocal + db_local = SessionLocal() + try: + service_local = CacheService(db_local) + service_local.fill_single_date_data(code, security_type, period_type, date_obj) + except Exception as e: + import logging + logging.getLogger(__name__).error(f"补齐单日数据失败: {str(e)}") + finally: + db_local.close() + + background_tasks.add_task(run_fill_task) + + return ResponseModel(data={"code": code, "trade_date": trade_date, "status": "processing"}) + + +@router.post("/fill-code-all", response_model=ResponseModel) +async def fill_all_dates_for_code( + request: dict, + background_tasks: BackgroundTasks, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """补齐单个代码的所有缺失交易日数据""" + service = CacheService(db) + + code = request.get("code") + security_type = request.get("security_type", "stock") + period_type = request.get("period_type", "daily") + start_date = request.get("start_date") + end_date = request.get("end_date") + missing_dates = request.get("missing_dates", []) + + if not code: + return ResponseModel(code=400, message="缺少必要参数: code") + + if not start_date or not end_date: + return ResponseModel(code=400, message="缺少必要参数: start_date 或 end_date") + + from app.utils.date_utils import parse_date + start_obj = parse_date(start_date) + end_obj = parse_date(end_date) + + def run_fill_all_task(): + from app.db.session import SessionLocal + db_local = SessionLocal() + try: + service_local = CacheService(db_local) + service_local.fill_all_dates_for_code(code, security_type, period_type, start_obj, end_obj, missing_dates) + except Exception as e: + import logging + logging.getLogger(__name__).error(f"补齐所有数据失败: {str(e)}") + finally: + db_local.close() + + background_tasks.add_task(run_fill_all_task) + + return ResponseModel(data={ + "code": code, + "missing_count": len(missing_dates), + "status": "processing" + }) diff --git a/backend/app/services/cache_service.py b/backend/app/services/cache_service.py index f6fc277..0c2024c 100644 --- a/backend/app/services/cache_service.py +++ b/backend/app/services/cache_service.py @@ -1104,3 +1104,151 @@ class CacheService: "min_date": None, "max_date": None } + + def get_missing_dates_for_code( + self, + code: str, + security_type: str, + period_type: str, + start_date: date, + end_date: date + ) -> Dict: + """ + 获取单个代码的缺失交易日详情 + + Args: + code: 证券代码 + security_type: 证券类型 (stock, future) + period_type: 周期类型 (daily, min1, etc.) + start_date: 开始日期 + end_date: 结束日期 + + Returns: + 缺失交易日详情 + """ + market = get_market_from_code(code) + trading_days = self.base_service.get_trading_calendar(market, start_date, end_date) + expected_dates = set(trading_days) + + actual_dates = set() + + if security_type == "stock" and period_type == "daily": + records = self.db.query(StockKlineDaily.trade_date).filter( + and_( + StockKlineDaily.code == code, + StockKlineDaily.trade_date >= start_date, + StockKlineDaily.trade_date <= end_date + ) + ).all() + actual_dates = set(r.trade_date for r in records) + elif security_type == "future" and period_type == "daily": + records = self.db.query(FutureKlineDaily.trade_date).filter( + and_( + FutureKlineDaily.code == code, + FutureKlineDaily.trade_date >= start_date, + FutureKlineDaily.trade_date <= end_date + ) + ).all() + actual_dates = set(r.trade_date for r in records) + + missing_dates = sorted(list(expected_dates - actual_dates)) + + missing_dates_list = [] + for d in missing_dates: + missing_dates_list.append({ + "date": format_date(d), + "date_obj": d.isoformat() + }) + + return { + "code": code, + "security_type": security_type, + "period_type": period_type, + "start_date": format_date(start_date), + "end_date": format_date(end_date), + "expected_count": len(expected_dates), + "actual_count": len(actual_dates), + "missing_count": len(missing_dates), + "missing_dates": missing_dates_list + } + + def fill_single_date_data( + self, + code: str, + security_type: str, + period_type: str, + trade_date: date + ): + """ + 补齐单个代码的单个交易日数据 + + Args: + code: 证券代码 + security_type: 证券类型 + period_type: 周期类型 + trade_date: 交易日 + """ + logger.info(f"补齐单日数据: {code} - {format_date(trade_date)}") + + try: + if security_type == "stock": + self.stock_service.get_kline([code], trade_date, trade_date, period_type) + elif security_type == "future": + self.future_service.get_kline([code], trade_date, trade_date, period_type) + + logger.info(f"补齐单日数据成功: {code} - {format_date(trade_date)}") + + except Exception as e: + logger.error(f"补齐单日数据失败: {code} - {format_date(trade_date)}, 错误: {str(e)}") + raise + + def fill_all_dates_for_code( + self, + code: str, + security_type: str, + period_type: str, + start_date: date, + end_date: date, + missing_dates: List[str] = None + ): + """ + 补齐单个代码的所有缺失交易日数据 + + Args: + code: 证券代码 + security_type: 证券类型 + period_type: 周期类型 + start_date: 开始日期 + end_date: 结束日期 + missing_dates: 缺失日期列表(可选,如果不提供则自动检测) + """ + logger.info(f"补齐所有数据: {code} - {format_date(start_date)} 到 {format_date(end_date)}") + + if missing_dates: + from app.utils.date_utils import parse_date + dates_to_fill = [parse_date(d) for d in missing_dates] + else: + result = self.get_missing_dates_for_code(code, security_type, period_type, start_date, end_date) + dates_to_fill = [parse_date(d["date"]) for d in result["missing_dates"]] + + if not dates_to_fill: + logger.info(f"没有缺失数据需要补齐: {code}") + return + + success_count = 0 + error_count = 0 + + for trade_date in dates_to_fill: + try: + if security_type == "stock": + self.stock_service.get_kline([code], trade_date, trade_date, period_type) + elif security_type == "future": + self.future_service.get_kline([code], trade_date, trade_date, period_type) + + success_count += 1 + + except Exception as e: + logger.error(f"补齐{code} - {format_date(trade_date)}失败: {str(e)}") + error_count += 1 + + logger.info(f"补齐完成: {code}, 成功{success_count}个, 失败{error_count}个") diff --git a/frontend/src/api/cache.ts b/frontend/src/api/cache.ts index 3d50988..ce7107d 100644 --- a/frontend/src/api/cache.ts +++ b/frontend/src/api/cache.ts @@ -78,3 +78,32 @@ export const fillMissingData = (data: { }) => { return cacheRequest.post('/cache/fill-missing', data) } + +export const getMissingDatesForCode = (code: string, params: { + security_type: string + period_type: string + start_date: string + end_date: string +}) => { + return request.get(`/cache/missing-dates/${code}`, { params }) +} + +export const fillSingleDateData = (data: { + code: string + trade_date: string + security_type: string + period_type: string +}) => { + return cacheRequest.post('/cache/fill-single-date', data) +} + +export const fillAllDatesForCode = (data: { + code: string + security_type: string + period_type: string + start_date: string + end_date: string + missing_dates?: string[] +}) => { + return cacheRequest.post('/cache/fill-code-all', data) +} diff --git a/frontend/src/views/CacheManager/DetectMissing.vue b/frontend/src/views/CacheManager/DetectMissing.vue index 2776079..0540ecc 100644 --- a/frontend/src/views/CacheManager/DetectMissing.vue +++ b/frontend/src/views/CacheManager/DetectMissing.vue @@ -204,7 +204,7 @@ - + @@ -221,9 +221,79 @@ /> + + + + + +
+ +

正在加载缺失交易日数据...

+
+ +
+ + {{ missingDatesDetail.code }} + {{ missingDatesDetail.expected_count }} + {{ missingDatesDetail.actual_count }} + + {{ missingDatesDetail.missing_count }} + + + +
+ 缺失交易日列表: + + 补齐所有缺失数据 + +
+ + + + + + + + + + +
+ +
+

暂无数据

+
+ + +
+