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 = ``;
+ 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. 修复批量导入 + 前端展示去重详情