diff --git a/app/api/trade_review.py b/app/api/trade_review.py index 0a2408d..893e52b 100644 --- a/app/api/trade_review.py +++ b/app/api/trade_review.py @@ -60,10 +60,12 @@ async def import_settlement( total_parsed = result['futures_parsed'] + result['options_parsed'] if total_new == 0 and total_skipped > 0: + skipped_details = result.get('skipped_details', []) return { "success": True, "message": f"全部跳过:共解析 {total_parsed} 条记录,均已存在(去重规则:交易日+品种+时间+价格),未导入任何新数据", "data": result, + "skipped_details": skipped_details, } msg_parts = [f"导入成功:期货 {result['futures_count']} 条,期权 {result['options_count']} 条"] @@ -75,6 +77,7 @@ async def import_settlement( "success": True, "message": ",".join(msg_parts), "data": result, + "skipped_details": result.get('skipped_details', []), } except HTTPException: raise @@ -145,6 +148,7 @@ async def batch_import_settlement( "message": ",".join(msg_parts), "new_count": file_new, "skipped_count": file_skipped, + "skipped_details": result.get('skipped_details', []), "trade_dates": result['trade_dates'], "batch_id": result['batch_id'], }) diff --git a/app/services/trade_parser.py b/app/services/trade_parser.py index 2c32d85..d8c1245 100644 --- a/app/services/trade_parser.py +++ b/app/services/trade_parser.py @@ -270,6 +270,7 @@ def save_to_db( records = [] new_keys_in_batch = set() # 防止本次导入文件内部重复 + skipped_details = [] # 记录被跳过的重复记录详情 # 处理期货记录 if not df_futures.empty: @@ -288,6 +289,13 @@ def save_to_db( dedup_key = _build_dedup_key(trade_date, variety, trade_time, price) if dedup_key in existing_keys or dedup_key in new_keys_in_batch: futures_skipped += 1 + skipped_details.append({ + 'type': '期货', + 'variety': variety, + 'date': trade_date, + 'time': trade_time, + 'price': price, + }) continue # 记录前几条的去重键用于调试 @@ -331,9 +339,18 @@ def save_to_db( dedup_key = _build_dedup_key(trade_date, variety, trade_time, price) if dedup_key in existing_keys or dedup_key in new_keys_in_batch: options_skipped += 1 + skipped_details.append({ + 'type': '期权', + 'variety': variety, + 'date': trade_date, + 'time': trade_time, + 'price': price, + }) continue new_keys_in_batch.add(dedup_key) + # 期权盈亏来自权利金(带正负号:卖为正,买为负) + premium = safe_float(row.get('权利金')) rec = TradeRecord( trade_type='期权', symbol=contract, @@ -343,8 +360,8 @@ def save_to_db( offset='', price=price, volume=safe_float(row.get('成交量')), - amount=safe_float(row.get('权利金')), - close_pnl=0.0, + amount=premium, + close_pnl=premium, # 期权盈亏 = 权利金(带正负号) commission=safe_float(row.get('手续费')), trade_date=trade_date, trade_time=trade_time, @@ -393,6 +410,7 @@ def save_to_db( "options_skipped": options_skipped, "futures_parsed": futures_parsed, "options_parsed": options_parsed, + "skipped_details": skipped_details, # 被跳过的重复记录详情 } diff --git a/app/static/futures_analysis.js b/app/static/futures_analysis.js index 5016d2b..5e36463 100644 --- a/app/static/futures_analysis.js +++ b/app/static/futures_analysis.js @@ -2326,7 +2326,7 @@ async function trHandleFileImport(e) { const res = await fetch(`${TR_API_BASE}/import`, { method: 'POST', body: formData }); const json = await res.json(); if (json.success) { - alert(json.message); + trShowImportResult(json.message, json.skipped_details || []); trLoadAllData(); } else { alert('导入失败: ' + (json.message || '未知错误')); @@ -2339,6 +2339,53 @@ async function trHandleFileImport(e) { } } +// 显示导入结果(含去重详情) +function trShowImportResult(message, skippedDetails) { + let html = `
+
${message}
`; + + if (skippedDetails && skippedDetails.length > 0) { + html += `
+
+ 被过滤的重复记录(${skippedDetails.length} 条): +
+
`; + + // 按类型分组显示 + const grouped = {}; + skippedDetails.forEach(d => { + const key = d.type || '未知'; + if (!grouped[key]) grouped[key] = []; + grouped[key].push(d); + }); + + for (const [type, items] of Object.entries(grouped)) { + html += `
${type}(${items.length} 条):
`; + items.slice(0, 10).forEach(d => { + html += `
• ${d.variety} ${d.date} ${d.time} @ ${d.price}
`; + }); + if (items.length > 10) { + html += `
... 还有 ${items.length - 10} 条
`; + } + } + + html += `
`; + } + + html += `
`; + + // 使用自定义弹窗 + const modal = document.createElement('div'); + modal.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:10000;display:flex;align-items:center;justify-content:center;'; + modal.innerHTML = `
+ ${html} +
+ +
+
`; + document.body.appendChild(modal); +} + async function trHandleBatchImport(e) { const files = e.target.files; if (!files || files.length === 0) return; @@ -2375,9 +2422,27 @@ function trShowBatchImportResult(json) { summaryEl.textContent = json.message; detailsEl.innerHTML = data.details.map(d => { let icon = d.success ? (d.new_count > 0 ? '✅' : '⚠️') : '❌'; + let skippedHtml = ''; + if (d.skipped_details && d.skipped_details.length > 0) { + const grouped = {}; + d.skipped_details.forEach(item => { + const key = item.type || '未知'; + if (!grouped[key]) grouped[key] = []; + grouped[key].push(item); + }); + skippedHtml = `
`; + for (const [type, items] of Object.entries(grouped)) { + skippedHtml += `
${type} ${items.length}条: `; + skippedHtml += items.slice(0, 5).map(item => `${item.variety}@${item.price}`).join(', '); + if (items.length > 5) skippedHtml += ` ...`; + skippedHtml += `
`; + } + skippedHtml += `
`; + } return `
${icon} ${d.filename}
${d.message}
+ ${skippedHtml}
`; }).join(''); modal.classList.add('show'); diff --git a/data/buffer.db b/data/buffer.db index 1b63c8d..181c135 100644 Binary files a/data/buffer.db and b/data/buffer.db differ diff --git a/data/futures_analysis.db b/data/futures_analysis.db index e842d73..922684f 100644 Binary files a/data/futures_analysis.db and b/data/futures_analysis.db differ diff --git a/openspec/changes/trade-import-fix/design.md b/openspec/changes/trade-import-fix/design.md new file mode 100644 index 0000000..e5882cb --- /dev/null +++ b/openspec/changes/trade-import-fix/design.md @@ -0,0 +1,14 @@ +# 设计文档 + +## 1. 去重详情提示 +- `save_to_db` 新增 `skipped_details` 列表,记录每条被跳过的记录:`{variety, date, time, price, type}` +- API 响应中包含此列表 +- 前端弹窗中展示被跳过的记录详情 + +## 2. 批量导入修复 +- 检查前端 FormData 传参方式 +- 确保后端 `files` 参数正确接收 + +## 3. 期权盈亏修复 +- 期权 `close_pnl` 改为使用 `权利金` 字段(带正负号:卖为正,买为负) +- `calc_daily_summary` 中期权盈亏公式:`pnl = amount - commission`(amount 即权利金) diff --git a/openspec/changes/trade-import-fix/proposal.md b/openspec/changes/trade-import-fix/proposal.md new file mode 100644 index 0000000..14e3103 --- /dev/null +++ b/openspec/changes/trade-import-fix/proposal.md @@ -0,0 +1,16 @@ +# 交易复盘导入修复 + +## 动机 +1. 去重过滤时只提示跳过了多少条,未说明具体是哪些记录被过滤 +2. 批量导入功能失败 +3. 期权交易盈亏计算错误(close_pnl 硬编码为 0,应使用权利金计算盈亏) + +## 目标 +1. 去重时记录被过滤记录的详细信息(品种、日期、时间、价格),并在前端展示 +2. 修复批量导入失败问题 +3. 修复期权盈亏计算:使用权利金(amount)作为期权盈亏来源 + +## 范围 +- `app/services/trade_parser.py` — 去重详情收集、期权盈亏修复 +- `app/api/trade_review.py` — 返回去重详情、批量导入修复 +- `app/static/futures_analysis.js` — 前端展示去重详情 diff --git a/openspec/changes/trade-import-fix/tasks.md b/openspec/changes/trade-import-fix/tasks.md new file mode 100644 index 0000000..795916a --- /dev/null +++ b/openspec/changes/trade-import-fix/tasks.md @@ -0,0 +1,5 @@ +# 任务列表 + +- [x] 1. 修复期权盈亏计算:`save_to_db` 中期权 `close_pnl` 改为使用权利金(带正负号) +- [x] 2. 收集去重详情:`save_to_db` 新增 `skipped_details` 列表,记录被跳过的记录详情 +- [x] 3. 修复批量导入 + 前端展示去重详情