Compare commits

...

8 Commits

@ -60,10 +60,12 @@ async def import_settlement(
total_parsed = result['futures_parsed'] + result['options_parsed'] total_parsed = result['futures_parsed'] + result['options_parsed']
if total_new == 0 and total_skipped > 0: if total_new == 0 and total_skipped > 0:
skipped_details = result.get('skipped_details', [])
return { return {
"success": True, "success": True,
"message": f"全部跳过:共解析 {total_parsed} 条记录,均已存在(去重规则:交易日+品种+时间+价格),未导入任何新数据", "message": f"全部跳过:共解析 {total_parsed} 条记录,均已存在(去重规则:交易日+品种+时间+价格),未导入任何新数据",
"data": result, "data": result,
"skipped_details": skipped_details,
} }
msg_parts = [f"导入成功:期货 {result['futures_count']} 条,期权 {result['options_count']}"] msg_parts = [f"导入成功:期货 {result['futures_count']} 条,期权 {result['options_count']}"]
@ -75,6 +77,7 @@ async def import_settlement(
"success": True, "success": True,
"message": "".join(msg_parts), "message": "".join(msg_parts),
"data": result, "data": result,
"skipped_details": result.get('skipped_details', []),
} }
except HTTPException: except HTTPException:
raise raise
@ -145,6 +148,7 @@ async def batch_import_settlement(
"message": "".join(msg_parts), "message": "".join(msg_parts),
"new_count": file_new, "new_count": file_new,
"skipped_count": file_skipped, "skipped_count": file_skipped,
"skipped_details": result.get('skipped_details', []),
"trade_dates": result['trade_dates'], "trade_dates": result['trade_dates'],
"batch_id": result['batch_id'], "batch_id": result['batch_id'],
}) })

@ -6,7 +6,7 @@ import re
import uuid import uuid
import json import json
import logging import logging
from datetime import datetime from datetime import datetime, timedelta
from collections import defaultdict from collections import defaultdict
from pathlib import Path from pathlib import Path
@ -194,6 +194,48 @@ def _normalize_time(time_val) -> str:
return s return s
def recalculate_trade_date(trade_date: str, trade_time: str) -> str:
"""
根据交易时间重新计算交易日
期货交易规则
- 前一交易日 21:00 之后到当前交易日 15:00 之前的交易算作当天交易
- 周五 21:00 之后的夜盘交易算作下周一的交易数据
:param trade_date: 原始交易日 (YYYY-MM-DD)
:param trade_time: 交易时间 (HH:MM:SS)
:return: 重新计算后的交易日
"""
if not trade_date or not trade_time:
return trade_date
try:
# 解析日期和时间
dt = datetime.strptime(trade_date, '%Y-%m-%d')
time_parts = trade_time.split(':')
hour = int(time_parts[0])
# 判断是否在夜盘时段 (21:00 之后)
if hour >= 21:
# 夜盘交易,交易日归属下一天
next_day = dt + timedelta(days=1)
# 如果是周五 (weekday=4),下一天是周六,需要跳到下周一
if dt.weekday() == 4: # 周五
next_day = dt + timedelta(days=3) # 跳到下周一
# 如果是周六 (weekday=5),跳到下周一
elif dt.weekday() == 5:
next_day = dt + timedelta(days=2)
# 如果是周日 (weekday=6),跳到下周一
elif dt.weekday() == 6:
next_day = dt + timedelta(days=1)
return next_day.strftime('%Y-%m-%d')
else:
# 日盘交易,保持原交易日
return trade_date
except (ValueError, IndexError):
return trade_date
def _build_dedup_key(trade_date: str, variety: str, trade_time: str, price: float) -> str: def _build_dedup_key(trade_date: str, variety: str, trade_time: str, price: float) -> str:
"""构建去重键:交易日+品种+交易时间+价格价格保留2位小数避免浮点精度问题""" """构建去重键:交易日+品种+交易时间+价格价格保留2位小数避免浮点精度问题"""
return f"{trade_date}|{variety}|{trade_time}|{round(price, 2)}" return f"{trade_date}|{variety}|{trade_time}|{round(price, 2)}"
@ -270,6 +312,7 @@ def save_to_db(
records = [] records = []
new_keys_in_batch = set() # 防止本次导入文件内部重复 new_keys_in_batch = set() # 防止本次导入文件内部重复
skipped_details = [] # 记录被跳过的重复记录详情
# 处理期货记录 # 处理期货记录
if not df_futures.empty: if not df_futures.empty:
@ -279,8 +322,10 @@ def save_to_db(
continue continue
futures_parsed += 1 futures_parsed += 1
variety = extract_variety(contract) variety = extract_variety(contract)
trade_date = _normalize_date(row.get('实际成交日期', '')) raw_date = _normalize_date(row.get('实际成交日期', ''))
trade_time = _normalize_time(row.get('成交时间', '')) trade_time = _normalize_time(row.get('成交时间', ''))
# 根据交易时间重新计算交易日(夜盘归属下一天)
trade_date = recalculate_trade_date(raw_date, trade_time)
bs_flag = str(row.get('买/卖', '')).strip() bs_flag = str(row.get('买/卖', '')).strip()
oc_flag = str(row.get('开/平', '')).strip() oc_flag = str(row.get('开/平', '')).strip()
price = safe_float(row.get('成交价')) price = safe_float(row.get('成交价'))
@ -288,11 +333,18 @@ def save_to_db(
dedup_key = _build_dedup_key(trade_date, variety, trade_time, price) 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: if dedup_key in existing_keys or dedup_key in new_keys_in_batch:
futures_skipped += 1 futures_skipped += 1
skipped_details.append({
'type': '期货',
'variety': variety,
'date': trade_date,
'time': trade_time,
'price': price,
})
continue continue
# 记录前几条的去重键用于调试 # 记录前几条的去重键用于调试
if futures_parsed <= 3: if futures_parsed <= 3:
logger.info(f"[去重校验] 期货记录 #{futures_parsed}: date={trade_date!r}, variety={variety!r}, time={trade_time!r}, price={price!r} => key={dedup_key!r}") logger.info(f"[去重校验] 期货记录 #{futures_parsed}: raw_date={raw_date!r}, trade_date={trade_date!r}, variety={variety!r}, time={trade_time!r}, price={price!r} => key={dedup_key!r}")
new_keys_in_batch.add(dedup_key) new_keys_in_batch.add(dedup_key)
rec = TradeRecord( rec = TradeRecord(
@ -323,17 +375,28 @@ def save_to_db(
continue continue
options_parsed += 1 options_parsed += 1
variety = extract_variety(contract) variety = extract_variety(contract)
trade_date = _normalize_date(row.get('成交日期', '')) raw_date = _normalize_date(row.get('成交日期', ''))
trade_time = _normalize_time(row.get('成交时间', '')) trade_time = _normalize_time(row.get('成交时间', ''))
# 根据交易时间重新计算交易日(夜盘归属下一天)
trade_date = recalculate_trade_date(raw_date, trade_time)
bs_flag = str(row.get('买/卖', '')).strip() bs_flag = str(row.get('买/卖', '')).strip()
price = safe_float(row.get('权利金单价')) price = safe_float(row.get('权利金单价'))
dedup_key = _build_dedup_key(trade_date, variety, trade_time, price) 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: if dedup_key in existing_keys or dedup_key in new_keys_in_batch:
options_skipped += 1 options_skipped += 1
skipped_details.append({
'type': '期权',
'variety': variety,
'date': trade_date,
'time': trade_time,
'price': price,
})
continue continue
new_keys_in_batch.add(dedup_key) new_keys_in_batch.add(dedup_key)
# 期权盈亏来自权利金(带正负号:卖为正,买为负)
premium = safe_float(row.get('权利金'))
rec = TradeRecord( rec = TradeRecord(
trade_type='期权', trade_type='期权',
symbol=contract, symbol=contract,
@ -343,8 +406,8 @@ def save_to_db(
offset='', offset='',
price=price, price=price,
volume=safe_float(row.get('成交量')), volume=safe_float(row.get('成交量')),
amount=safe_float(row.get('权利金')), amount=premium,
close_pnl=0.0, close_pnl=premium, # 期权盈亏 = 权利金(带正负号)
commission=safe_float(row.get('手续费')), commission=safe_float(row.get('手续费')),
trade_date=trade_date, trade_date=trade_date,
trade_time=trade_time, trade_time=trade_time,
@ -393,6 +456,7 @@ def save_to_db(
"options_skipped": options_skipped, "options_skipped": options_skipped,
"futures_parsed": futures_parsed, "futures_parsed": futures_parsed,
"options_parsed": options_parsed, "options_parsed": options_parsed,
"skipped_details": skipped_details, # 被跳过的重复记录详情
} }

@ -594,6 +594,24 @@
.tr-tab-panel { display: none; } .tr-tab-panel { display: none; }
.tr-tab-panel.active { display: block; } .tr-tab-panel.active { display: block; }
/* 快捷日期按钮 */
.tr-quick-dates { display: flex; gap: 8px; margin-bottom: 16px; }
.tr-quick-date { padding: 6px 16px; border-radius: 20px; border: 1px solid #e0e0e0; background: #fff; color: var(--text-secondary); font-size: 13px; cursor: pointer; transition: all 0.2s; }
.tr-quick-date:hover { border-color: var(--color-brand); color: var(--color-brand); }
.tr-quick-date.active { background: var(--color-brand); color: #fff; border-color: var(--color-brand); }
/* 数据管理 Tab 样式 */
.tr-data-management { background: var(--bg-card); border-radius: var(--radius-card); padding: 32px; box-shadow: var(--shadow-sm); }
.tr-section-title { font-size: 16px; font-weight: 600; color: var(--text-primary); margin-bottom: 16px; padding-bottom: 8px; border-bottom: 1px solid rgba(0,0,0,0.05); }
.tr-import-section { display: flex; gap: 12px; align-items: center; }
.tr-data-operations { display: flex; flex-direction: column; gap: 16px; }
.tr-operation-item { display: flex; align-items: center; gap: 12px; }
.tr-operation-item label { font-size: 14px; color: var(--text-secondary); min-width: 120px; }
.tr-data-overview { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
.tr-overview-card { background: #F5F5F7; border-radius: 12px; padding: 20px; text-align: center; }
.tr-overview-label { font-size: 12px; color: var(--text-tertiary); margin-bottom: 8px; font-weight: 500; }
.tr-overview-value { font-size: 20px; font-weight: 700; color: var(--text-primary); }
/* 交易反思 - 日期导航 */ /* 交易反思 - 日期导航 */
.tr-refl-datebar { display: flex; align-items: center; justify-content: center; gap: 16px; margin-bottom: 24px; } .tr-refl-datebar { display: flex; align-items: center; justify-content: center; gap: 16px; margin-bottom: 24px; }
.tr-refl-datebtn { width: 36px; height: 36px; border-radius: 50%; border: none; background: #fff; color: var(--text-secondary); cursor: pointer; box-shadow: var(--shadow-sm); transition: all 0.2s; font-size: 16px; } .tr-refl-datebtn { width: 36px; height: 36px; border-radius: 50%; border: none; background: #fff; color: var(--text-secondary); cursor: pointer; box-shadow: var(--shadow-sm); transition: all 0.2s; font-size: 16px; }
@ -1114,13 +1132,14 @@
<button class="tr-subtab active" data-subtab="stats">统计分析</button> <button class="tr-subtab active" data-subtab="stats">统计分析</button>
<button class="tr-subtab" data-subtab="reflection">交易反思</button> <button class="tr-subtab" data-subtab="reflection">交易反思</button>
<button class="tr-subtab" data-subtab="experience">经验库</button> <button class="tr-subtab" data-subtab="experience">经验库</button>
<button class="tr-subtab" data-subtab="data">数据管理</button>
</div> </div>
<!-- 统计分析子 Tab --> <!-- 数据管理子 Tab -->
<div id="tr-tab-stats" class="tr-tab-panel active"> <div id="tr-tab-data" class="tr-tab-panel">
<!-- 工具栏 --> <div class="tr-data-management">
<div class="tr-toolbar"> <div class="tr-section-title">结算单导入</div>
<div class="tr-toolbar-left"> <div class="tr-import-section">
<input type="file" id="tr-file-input" accept=".xls,.xlsx" style="display:none" /> <input type="file" id="tr-file-input" accept=".xls,.xlsx" style="display:none" />
<input type="file" id="tr-batch-file-input" accept=".xls,.xlsx" multiple style="display:none" /> <input type="file" id="tr-batch-file-input" accept=".xls,.xlsx" multiple style="display:none" />
<button id="tr-btn-import" class="tr-btn tr-btn-primary"> <button id="tr-btn-import" class="tr-btn tr-btn-primary">
@ -1129,21 +1148,68 @@
<button id="tr-btn-batch-import" class="tr-btn tr-btn-primary" title="同时导入多个结算单文件"> <button id="tr-btn-batch-import" class="tr-btn tr-btn-primary" title="同时导入多个结算单文件">
<span>📂</span><span>批量导入</span> <span>📂</span><span>批量导入</span>
</button> </button>
<input type="date" id="tr-start-date" class="tr-date-input" title="开始日期" />
<span style="color:var(--text-tertiary);font-size:13px;">~</span>
<input type="date" id="tr-end-date" class="tr-date-input" title="结束日期" />
<button id="tr-btn-query" class="tr-btn tr-btn-secondary">查询</button>
</div> </div>
<div class="tr-toolbar-right">
<button id="tr-btn-delete-date" class="tr-btn tr-btn-danger" title="删除当前查询日期的所有交易记录"> <div class="tr-section-title" style="margin-top:32px;">数据操作</div>
<span>🗑️</span><span>删除当日</span> <div class="tr-data-operations">
<div class="tr-operation-item">
<label>删除指定日期数据:</label>
<input type="date" id="tr-delete-date-input" class="tr-date-input" />
<button id="tr-btn-delete-date" class="tr-btn tr-btn-danger" title="删除指定日期的所有交易记录">
<span>🗑️</span><span>删除</span>
</button> </button>
</div>
<div class="tr-operation-item">
<label>批次管理:</label>
<button id="tr-btn-batches" class="tr-btn tr-btn-secondary"> <button id="tr-btn-batches" class="tr-btn tr-btn-secondary">
<span>📋</span><span>批次管理</span> <span>📋</span><span>查看导入批次</span>
</button> </button>
</div> </div>
</div> </div>
<div class="tr-section-title" style="margin-top:32px;">数据概览</div>
<div id="tr-data-overview" class="tr-data-overview">
<div class="tr-overview-card">
<div class="tr-overview-label">总交易记录</div>
<div class="tr-overview-value" id="tr-total-records">-</div>
</div>
<div class="tr-overview-card">
<div class="tr-overview-label">导入批次</div>
<div class="tr-overview-value" id="tr-total-batches">-</div>
</div>
<div class="tr-overview-card">
<div class="tr-overview-label">最早日期</div>
<div class="tr-overview-value" id="tr-earliest-date">-</div>
</div>
<div class="tr-overview-card">
<div class="tr-overview-label">最新日期</div>
<div class="tr-overview-value" id="tr-latest-date">-</div>
</div>
</div>
</div>
</div>
<!-- 统计分析子 Tab -->
<div id="tr-tab-stats" class="tr-tab-panel active">
<!-- 快捷日期 -->
<div class="tr-quick-dates" id="tr-quick-dates">
<button class="tr-quick-date active" data-range="today">当日</button>
<button class="tr-quick-date" data-range="week">近一周</button>
<button class="tr-quick-date" data-range="half-month">近半月</button>
<button class="tr-quick-date" data-range="month">近一月</button>
<button class="tr-quick-date" data-range="quarter">近三月</button>
<button class="tr-quick-date" data-range="custom">指定日期</button>
</div>
<!-- 工具栏 -->
<div class="tr-toolbar" id="tr-stats-toolbar">
<div class="tr-toolbar-left">
<input type="date" id="tr-start-date" class="tr-date-input" title="开始日期" />
<span style="color:var(--text-tertiary);font-size:13px;">~</span>
<input type="date" id="tr-end-date" class="tr-date-input" title="结束日期" />
<button id="tr-btn-query" class="tr-btn tr-btn-secondary">查询</button>
</div>
</div>
<!-- 6个统计卡片 --> <!-- 6个统计卡片 -->
<div id="tr-stats-grid" class="tr-stats-grid"></div> <div id="tr-stats-grid" class="tr-stats-grid"></div>

@ -651,6 +651,7 @@ function trSwitchSubtab(tab) {
panel.classList.toggle('active', panel.id === 'tr-tab-' + tab); panel.classList.toggle('active', panel.id === 'tr-tab-' + tab);
}); });
if (tab === 'experience') trExpLoad(); if (tab === 'experience') trExpLoad();
if (tab === 'data') trLoadDataOverview();
} }
function trSwitchToReflection(date) { function trSwitchToReflection(date) {
@ -1642,6 +1643,58 @@ async function trInitDefaultDate() {
} catch (e) { } catch (e) {
console.error('获取最后交易日失败', e); console.error('获取最后交易日失败', e);
} }
// 初始化快捷日期按钮
trInitQuickDates();
trLoadAllData();
}
function trInitQuickDates() {
document.querySelectorAll('#tr-quick-dates .tr-quick-date').forEach(btn => {
btn.addEventListener('click', () => trSetDateRange(btn.dataset.range));
});
}
function trSetDateRange(range) {
// 更新按钮状态
document.querySelectorAll('#tr-quick-dates .tr-quick-date').forEach(btn => {
btn.classList.toggle('active', btn.dataset.range === range);
});
const today = new Date();
const end = today.toISOString().split('T')[0];
let start = end;
switch (range) {
case 'today':
start = end;
break;
case 'week':
const weekAgo = new Date(today);
weekAgo.setDate(weekAgo.getDate() - 7);
start = weekAgo.toISOString().split('T')[0];
break;
case 'half-month':
const halfMonthAgo = new Date(today);
halfMonthAgo.setDate(halfMonthAgo.getDate() - 15);
start = halfMonthAgo.toISOString().split('T')[0];
break;
case 'month':
const monthAgo = new Date(today);
monthAgo.setMonth(monthAgo.getMonth() - 1);
start = monthAgo.toISOString().split('T')[0];
break;
case 'quarter':
const quarterAgo = new Date(today);
quarterAgo.setMonth(quarterAgo.getMonth() - 3);
start = quarterAgo.toISOString().split('T')[0];
break;
case 'custom':
// 自定义模式,不自动设置日期,让用户手动选择
return;
}
document.getElementById('tr-start-date').value = start;
document.getElementById('tr-end-date').value = end;
trLoadAllData(); trLoadAllData();
} }
@ -2326,8 +2379,9 @@ async function trHandleFileImport(e) {
const res = await fetch(`${TR_API_BASE}/import`, { method: 'POST', body: formData }); const res = await fetch(`${TR_API_BASE}/import`, { method: 'POST', body: formData });
const json = await res.json(); const json = await res.json();
if (json.success) { if (json.success) {
alert(json.message); trShowImportResult(json.message, json.skipped_details || []);
trLoadAllData(); trLoadAllData();
trLoadDataOverview();
} else { } else {
alert('导入失败: ' + (json.message || '未知错误')); alert('导入失败: ' + (json.message || '未知错误'));
} }
@ -2339,6 +2393,53 @@ async function trHandleFileImport(e) {
} }
} }
// 显示导入结果(含去重详情)
function trShowImportResult(message, skippedDetails) {
let html = `<div style="padding:16px;max-width:500px;">
<div style="font-size:14px;margin-bottom:12px;">${message}</div>`;
if (skippedDetails && skippedDetails.length > 0) {
html += `<div style="margin-top:12px;border-top:1px solid #eee;padding-top:12px;">
<div style="font-weight:600;font-size:13px;margin-bottom:8px;color:#666;">
被过滤的重复记录${skippedDetails.length}
</div>
<div style="max-height:200px;overflow-y:auto;font-size:12px;background:#f9f9f9;border-radius:4px;padding:8px;">`;
// 按类型分组显示
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 += `<div style="margin-bottom:6px;"><strong>${type}</strong>${items.length} 条):</div>`;
items.slice(0, 10).forEach(d => {
html += `<div style="padding:2px 0;color:#555;">• ${d.variety} ${d.date} ${d.time} @ ${d.price}</div>`;
});
if (items.length > 10) {
html += `<div style="color:#999;">... 还有 ${items.length - 10} 条</div>`;
}
}
html += `</div></div>`;
}
html += `</div>`;
// 使用自定义弹窗
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 = `<div style="background:white;border-radius:8px;max-width:600px;max-height:80vh;overflow-y:auto;">
${html}
<div style="padding:12px 16px;border-top:1px solid #eee;text-align:right;">
<button onclick="this.closest('div[style*=fixed]').remove()" style="padding:6px 16px;background:#007aff;color:white;border:none;border-radius:4px;cursor:pointer;">确定</button>
</div>
</div>`;
document.body.appendChild(modal);
}
async function trHandleBatchImport(e) { async function trHandleBatchImport(e) {
const files = e.target.files; const files = e.target.files;
if (!files || files.length === 0) return; if (!files || files.length === 0) return;
@ -2355,7 +2456,10 @@ async function trHandleBatchImport(e) {
const json = await res.json(); const json = await res.json();
if (json.success) { if (json.success) {
trShowBatchImportResult(json); trShowBatchImportResult(json);
if (json.data.total_futures > 0 || json.data.total_options > 0) trLoadAllData(); if (json.data.total_futures > 0 || json.data.total_options > 0) {
trLoadAllData();
trLoadDataOverview();
}
} else { } else {
alert('批量导入失败: ' + (json.message || '未知错误')); alert('批量导入失败: ' + (json.message || '未知错误'));
} }
@ -2375,9 +2479,27 @@ function trShowBatchImportResult(json) {
summaryEl.textContent = json.message; summaryEl.textContent = json.message;
detailsEl.innerHTML = data.details.map(d => { detailsEl.innerHTML = data.details.map(d => {
let icon = d.success ? (d.new_count > 0 ? '✅' : '⚠️') : '❌'; 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 = `<div style="margin-top:6px;font-size:11px;color:#888;max-height:80px;overflow-y:auto;">`;
for (const [type, items] of Object.entries(grouped)) {
skippedHtml += `<div><strong>${type}</strong> ${items.length}条: `;
skippedHtml += items.slice(0, 5).map(item => `${item.variety}@${item.price}`).join(', ');
if (items.length > 5) skippedHtml += ` ...`;
skippedHtml += `</div>`;
}
skippedHtml += `</div>`;
}
return `<div style="padding:10px 12px;margin-bottom:8px;border-radius:6px;background:#F5F5F7;border-left:3px solid ${d.success ? '#34C759' : '#FF3B30'};"> return `<div style="padding:10px 12px;margin-bottom:8px;border-radius:6px;background:#F5F5F7;border-left:3px solid ${d.success ? '#34C759' : '#FF3B30'};">
<div style="font-weight:600;font-size:13px;">${icon} ${d.filename}</div> <div style="font-weight:600;font-size:13px;">${icon} ${d.filename}</div>
<div style="font-size:12px;color:var(--text-tertiary);margin-top:4px;">${d.message}</div> <div style="font-size:12px;color:var(--text-tertiary);margin-top:4px;">${d.message}</div>
${skippedHtml}
</div>`; </div>`;
}).join(''); }).join('');
modal.classList.add('show'); modal.classList.add('show');
@ -2434,11 +2556,8 @@ function trCloseBatchModal() {
// ===== 删除当日 ===== // ===== 删除当日 =====
async function trDeleteByDate() { async function trDeleteByDate() {
const startDate = document.getElementById('tr-start-date').value; const tradeDate = document.getElementById('tr-delete-date-input').value;
const endDate = document.getElementById('tr-end-date').value; if (!tradeDate) { alert('请先选择要删除的日期'); return; }
if (!startDate || !endDate) { alert('请先选择要删除的日期范围'); return; }
if (startDate !== endDate) { alert('删除功能仅支持单个日期,请确保开始日期和结束日期相同'); return; }
const tradeDate = startDate;
try { try {
const queryRes = await fetch(`${TR_API_BASE}/records?start_date=${tradeDate}&end_date=${tradeDate}&page=1&page_size=1`); const queryRes = await fetch(`${TR_API_BASE}/records?start_date=${tradeDate}&end_date=${tradeDate}&page=1&page_size=1`);
@ -2453,12 +2572,52 @@ async function trDeleteByDate() {
if (json.success) { if (json.success) {
alert(`${json.message}`); alert(`${json.message}`);
trLoadAllData(); trLoadAllData();
trLoadDataOverview();
} else { } else {
alert(json.message || '删除失败'); alert(json.message || '删除失败');
} }
} catch (e) { alert('删除失败: ' + e.message); } } catch (e) { alert('删除失败: ' + e.message); }
} }
// ===== 数据概览 =====
async function trLoadDataOverview() {
try {
// 获取总记录数和日期范围
const res = await fetch(`${TR_API_BASE}/statistics`);
const json = await res.json();
if (json.success && json.data) {
document.getElementById('tr-total-records').textContent = json.data.total_trades || 0;
document.getElementById('tr-total-batches').textContent = json.data.trading_days || 0;
}
// 获取批次信息
const batchRes = await fetch(`${TR_API_BASE}/batches`);
const batchJson = await batchRes.json();
if (batchJson.success && batchJson.data && batchJson.data.length > 0) {
document.getElementById('tr-total-batches').textContent = batchJson.data.length;
// 获取最早和最新日期
const dates = batchJson.data.map(b => b.trade_dates).filter(d => d);
if (dates.length > 0) {
const allDates = [];
dates.forEach(d => {
const parts = d.split(' ~ ');
parts.forEach(p => {
const date = p.trim();
if (date) allDates.push(date);
});
});
if (allDates.length > 0) {
allDates.sort();
document.getElementById('tr-earliest-date').textContent = allDates[0];
document.getElementById('tr-latest-date').textContent = allDates[allDates.length - 1];
}
}
}
} catch (e) {
console.error('加载数据概览失败:', e);
}
}
function formatNumber(num) { function formatNumber(num) {
if (num === 0 || num === undefined || num === null) return '--'; if (num === 0 || num === undefined || num === null) return '--';
return num.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 2 }); return num.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 2 });

Binary file not shown.

Binary file not shown.

@ -0,0 +1,14 @@
# 数据管理模块独立
## 动机
交易复盘中的导入、批量导入、删除当日等功能混在统计分析中,不够清晰。需要将这些数据操作功能独立到一个专门的模块中。
## 目标
1. 新增"数据管理"子 Tab
2. 将导入、批量导入、删除当日、批次管理等功能移到数据管理模块
3. 统计分析模块只保留查询功能
4. 新增数据概览功能,显示总记录数、批次数量、日期范围等
## 范围
- `app/static/futures_analysis.html` — 新增数据管理 Tab 结构和样式
- `app/static/futures_analysis.js` — 修改删除逻辑、新增数据概览加载函数

@ -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 即权利金)

@ -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` — 前端展示去重详情

@ -0,0 +1,5 @@
# 任务列表
- [x] 1. 修复期权盈亏计算:`save_to_db` 中期权 `close_pnl` 改为使用权利金(带正负号)
- [x] 2. 收集去重详情:`save_to_db` 新增 `skipped_details` 列表,记录被跳过的记录详情
- [x] 3. 修复批量导入 + 前端展示去重详情

@ -0,0 +1,19 @@
# 设计文档
## 实现方案
### 交易日重计算函数 `recalculate_trade_date`
输入:原始交易日 (YYYY-MM-DD) + 交易时间 (HH:MM:SS)
输出:重新计算后的交易日
逻辑:
1. 解析交易时间,判断是否在 21:00 之后
2. 如果在 21:00 之后(夜盘):
- 如果原始交易日是周五,则交易日 = 下周一
- 否则交易日 = 原始交易日 + 1 天
3. 如果在 15:00 之前且不在夜盘时段,保持原交易日
4. 其他情况保持原交易日
### 应用位置
`save_to_db` 中处理每条记录时,调用 `recalculate_trade_date` 重新计算交易日。

@ -0,0 +1,14 @@
# 交易日定义重新定义
## 动机
当前交易日直接使用结算单中的"实际成交日期",但期货交易有夜盘,需要重新定义交易日归属:
- 前一交易日 21:00 之后到当前交易日 15:00 之前的交易,算作当天交易
- 周五 21:00 之后的夜盘交易,算作下周一的交易数据
## 目标
1. 在导入时根据交易时间重新计算交易日
2. 正确处理周五夜盘归属下周一的情况
## 范围
- `app/services/trade_parser.py` — 新增交易日重计算函数
- 导入时应用新的交易日规则

@ -0,0 +1,5 @@
# 任务列表
- [x] 1. 新增交易日重计算函数 `recalculate_trade_date`
- [x] 2. 在 `save_to_db` 中应用新的交易日规则
- [x] 3. 测试验证交易日计算逻辑

@ -0,0 +1,13 @@
# 统计分析快捷日期查询
## 动机
当前统计分析需要手动选择日期范围,不够便捷。需要添加快捷日期按钮,支持常用时间范围快速查询。
## 目标
1. 添加快捷日期按钮:当日、近一周、近半月、近一月、近三月、指定日期
2. 点击按钮自动设置日期范围并刷新数据
3. 将数据管理 Tab 移到经验库之后
## 范围
- `app/static/futures_analysis.html` — 添加快捷日期按钮、调整 Tab 顺序
- `app/static/futures_analysis.js` — 添加日期范围设置逻辑
Loading…
Cancel
Save