Compare commits

..

8 Commits

@ -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'],
})

@ -6,7 +6,7 @@ import re
import uuid
import json
import logging
from datetime import datetime
from datetime import datetime, timedelta
from collections import defaultdict
from pathlib import Path
@ -194,6 +194,48 @@ def _normalize_time(time_val) -> str:
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:
"""构建去重键:交易日+品种+交易时间+价格价格保留2位小数避免浮点精度问题"""
return f"{trade_date}|{variety}|{trade_time}|{round(price, 2)}"
@ -270,6 +312,7 @@ def save_to_db(
records = []
new_keys_in_batch = set() # 防止本次导入文件内部重复
skipped_details = [] # 记录被跳过的重复记录详情
# 处理期货记录
if not df_futures.empty:
@ -279,8 +322,10 @@ def save_to_db(
continue
futures_parsed += 1
variety = extract_variety(contract)
trade_date = _normalize_date(row.get('实际成交日期', ''))
raw_date = _normalize_date(row.get('实际成交日期', ''))
trade_time = _normalize_time(row.get('成交时间', ''))
# 根据交易时间重新计算交易日(夜盘归属下一天)
trade_date = recalculate_trade_date(raw_date, trade_time)
bs_flag = str(row.get('买/卖', '')).strip()
oc_flag = str(row.get('开/平', '')).strip()
price = safe_float(row.get('成交价'))
@ -288,11 +333,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:
futures_skipped += 1
skipped_details.append({
'type': '期货',
'variety': variety,
'date': trade_date,
'time': trade_time,
'price': price,
})
continue
# 记录前几条的去重键用于调试
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)
rec = TradeRecord(
@ -323,17 +375,28 @@ def save_to_db(
continue
options_parsed += 1
variety = extract_variety(contract)
trade_date = _normalize_date(row.get('成交日期', ''))
raw_date = _normalize_date(row.get('成交日期', ''))
trade_time = _normalize_time(row.get('成交时间', ''))
# 根据交易时间重新计算交易日(夜盘归属下一天)
trade_date = recalculate_trade_date(raw_date, trade_time)
bs_flag = str(row.get('买/卖', '')).strip()
price = safe_float(row.get('权利金单价'))
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 +406,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 +456,7 @@ def save_to_db(
"options_skipped": options_skipped,
"futures_parsed": futures_parsed,
"options_parsed": options_parsed,
"skipped_details": skipped_details, # 被跳过的重复记录详情
}

@ -594,6 +594,24 @@
.tr-tab-panel { display: none; }
.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-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" data-subtab="reflection">交易反思</button>
<button class="tr-subtab" data-subtab="experience">经验库</button>
<button class="tr-subtab" data-subtab="data">数据管理</button>
</div>
<!-- 统计分析子 Tab -->
<div id="tr-tab-stats" class="tr-tab-panel active">
<!-- 工具栏 -->
<div class="tr-toolbar">
<div class="tr-toolbar-left">
<!-- 数据管理子 Tab -->
<div id="tr-tab-data" class="tr-tab-panel">
<div class="tr-data-management">
<div class="tr-section-title">结算单导入</div>
<div class="tr-import-section">
<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" />
<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="同时导入多个结算单文件">
<span>📂</span><span>批量导入</span>
</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 class="tr-toolbar-right">
<button id="tr-btn-delete-date" class="tr-btn tr-btn-danger" title="删除当前查询日期的所有交易记录">
<span>🗑️</span><span>删除当日</span>
<div class="tr-section-title" style="margin-top:32px;">数据操作</div>
<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>
</div>
<div class="tr-operation-item">
<label>批次管理:</label>
<button id="tr-btn-batches" class="tr-btn tr-btn-secondary">
<span>📋</span><span>批次管理</span>
<span>📋</span><span>查看导入批次</span>
</button>
</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个统计卡片 -->
<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);
});
if (tab === 'experience') trExpLoad();
if (tab === 'data') trLoadDataOverview();
}
function trSwitchToReflection(date) {
@ -1642,6 +1643,58 @@ async function trInitDefaultDate() {
} catch (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();
}
@ -2326,8 +2379,9 @@ 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();
trLoadDataOverview();
} else {
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) {
const files = e.target.files;
if (!files || files.length === 0) return;
@ -2355,7 +2456,10 @@ async function trHandleBatchImport(e) {
const json = await res.json();
if (json.success) {
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 {
alert('批量导入失败: ' + (json.message || '未知错误'));
}
@ -2375,9 +2479,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 = `<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'};">
<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>
${skippedHtml}
</div>`;
}).join('');
modal.classList.add('show');
@ -2434,11 +2556,8 @@ function trCloseBatchModal() {
// ===== 删除当日 =====
async function trDeleteByDate() {
const startDate = document.getElementById('tr-start-date').value;
const endDate = document.getElementById('tr-end-date').value;
if (!startDate || !endDate) { alert('请先选择要删除的日期范围'); return; }
if (startDate !== endDate) { alert('删除功能仅支持单个日期,请确保开始日期和结束日期相同'); return; }
const tradeDate = startDate;
const tradeDate = document.getElementById('tr-delete-date-input').value;
if (!tradeDate) { alert('请先选择要删除的日期'); return; }
try {
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) {
alert(`${json.message}`);
trLoadAllData();
trLoadDataOverview();
} else {
alert(json.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) {
if (num === 0 || num === undefined || num === null) return '--';
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