feat: 新增了缺失交易日的详情查看和补齐功能

master
Lxy 1 month ago
parent 4db22a6f77
commit 7307fe6182

@ -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"
})

@ -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}")

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

@ -204,7 +204,7 @@
<template #header>
<span>缺失代码列表前100个</span>
</template>
<el-table :data="detectResult.missing_codes" stripe height="300">
<el-table :data="detectResult.missing_codes" stripe height="300" @row-click="handleRowClick">
<el-table-column prop="code" label="代码" width="120" />
<el-table-column prop="expected_count" label="预期天数" width="100" />
<el-table-column prop="actual_count" label="实际天数" width="100" />
@ -221,9 +221,79 @@
/>
</template>
</el-table-column>
<el-table-column label="操作" width="120">
<template #default="{ row }">
<el-button type="primary" size="small" @click.stop="handleRowClick(row)">
详情
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 缺失交易日详情弹窗 -->
<el-dialog
v-model="detailDialogVisible"
:title="`缺失交易日详情 - ${selectedCode?.code || ''}`"
width="800px"
destroy-on-close
>
<div v-if="loadingDetail" style="text-align: center; padding: 20px;">
<el-icon class="is-loading" :size="40"><Loading /></el-icon>
<p>正在加载缺失交易日数据...</p>
</div>
<div v-else-if="missingDatesDetail">
<el-descriptions :column="4" border style="margin-bottom: 20px;">
<el-descriptions-item label="代码">{{ missingDatesDetail.code }}</el-descriptions-item>
<el-descriptions-item label="预期天数">{{ missingDatesDetail.expected_count }}</el-descriptions-item>
<el-descriptions-item label="实际天数">{{ missingDatesDetail.actual_count }}</el-descriptions-item>
<el-descriptions-item label="缺失天数">
<el-tag type="warning">{{ missingDatesDetail.missing_count }}</el-tag>
</el-descriptions-item>
</el-descriptions>
<div style="margin-bottom: 15px; display: flex; justify-content: space-between; align-items: center;">
<span>缺失交易日列表</span>
<el-button type="success" @click="handleFillAllForCode" :loading="fillingAllForCode">
<el-icon><Download /></el-icon>
</el-button>
</div>
<el-table :data="missingDatesDetail.missing_dates" stripe height="400" v-loading="fillingSingleDate">
<el-table-column prop="date" label="交易日" width="120" />
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.filling ? 'warning' : row.filled ? 'success' : 'info'" size="small">
{{ row.filling ? '补齐中' : row.filled ? '已补齐' : '待补齐' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120">
<template #default="{ row }">
<el-button
type="primary"
size="small"
@click="handleFillSingleDate(row)"
:loading="row.filling"
:disabled="row.filled"
>
补齐
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div v-else style="text-align: center; padding: 20px; color: #999;">
<p>暂无数据</p>
</div>
<template #footer>
<el-button @click="detailDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
<!-- 批量检测结果 -->
<el-card class="result-card" v-if="batchDetectResult.length > 0">
<template #header>
@ -255,7 +325,8 @@
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { detectMissingData, batchCacheData, detectAllMissingData, cacheAllMissingData, getCacheTask, fillMissingData } from '@/api/cache'
import { Loading } from '@element-plus/icons-vue'
import { detectMissingData, batchCacheData, detectAllMissingData, cacheAllMissingData, getCacheTask, fillMissingData, getMissingDatesForCode, fillSingleDateData, fillAllDatesForCode } from '@/api/cache'
const detecting = ref(false)
const caching = ref(false)
@ -276,6 +347,13 @@ const wsProgress = reactive({
complete: 0
})
const detailDialogVisible = ref(false)
const selectedCode = ref<any>(null)
const loadingDetail = ref(false)
const missingDatesDetail = ref<any>(null)
const fillingSingleDate = ref(false)
const fillingAllForCode = ref(false)
let ws: WebSocket | null = null
const hasMissing = computed(() => batchDetectResult.value.some(r => r.missingCount > 0))
@ -594,6 +672,116 @@ const handleCache = async () => {
const showDetail = (row: any) => {
console.log(row.details)
}
const handleRowClick = async (row: any) => {
selectedCode.value = row
detailDialogVisible.value = true
loadingDetail.value = true
missingDatesDetail.value = null
try {
const res: any = await getMissingDatesForCode(row.code, {
security_type: form.securityType,
period_type: form.periodType,
start_date: form.startDate,
end_date: form.endDate
})
if (res.data) {
missingDatesDetail.value = {
...res.data,
missing_dates: res.data.missing_dates.map((d: any) => ({
...d,
filling: false,
filled: false
}))
}
} else {
ElMessage.error(res.message || '获取缺失交易日失败')
}
} catch (error: any) {
console.error(error)
const errorMsg = error.response?.data?.message || error.message || '网络请求失败'
ElMessage.error(`获取缺失交易日失败: ${errorMsg}`)
} finally {
loadingDetail.value = false
}
}
const handleFillSingleDate = async (row: any) => {
row.filling = true
fillingSingleDate.value = true
try {
const res: any = await fillSingleDateData({
code: missingDatesDetail.value.code,
trade_date: row.date,
security_type: form.securityType,
period_type: form.periodType
})
if (res.data) {
row.filled = true
ElMessage.success(`${row.date} 补齐任务已启动`)
} else {
ElMessage.error(res.message || '补齐失败')
}
} catch (error: any) {
console.error(error)
const errorMsg = error.response?.data?.message || error.message || '网络请求失败'
ElMessage.error(`补齐失败: ${errorMsg}`)
} finally {
row.filling = false
fillingSingleDate.value = false
}
}
const handleFillAllForCode = async () => {
if (!missingDatesDetail.value || missingDatesDetail.value.missing_dates.length === 0) {
ElMessage.warning('没有缺失数据需要补齐')
return
}
fillingAllForCode.value = true
const missingDates = missingDatesDetail.value.missing_dates
.filter((d: any) => !d.filled)
.map((d: any) => d.date)
if (missingDates.length === 0) {
ElMessage.info('所有缺失数据已补齐')
fillingAllForCode.value = false
return
}
try {
const res: any = await fillAllDatesForCode({
code: missingDatesDetail.value.code,
security_type: form.securityType,
period_type: form.periodType,
start_date: form.startDate,
end_date: form.endDate,
missing_dates: missingDates
})
if (res.data) {
ElMessage.success(`${missingDatesDetail.value.code} 补齐任务已启动,共 ${res.data.missing_count} 个交易日`)
missingDatesDetail.value.missing_dates.forEach((d: any) => {
if (!d.filled) {
d.filled = true
}
})
} else {
ElMessage.error(res.message || '补齐失败')
}
} catch (error: any) {
console.error(error)
const errorMsg = error.response?.data?.message || error.message || '网络请求失败'
ElMessage.error(`补齐失败: ${errorMsg}`)
} finally {
fillingAllForCode.value = false
}
}
</script>
<style scoped>

Loading…
Cancel
Save