fix: 导入时不校验本次数据重复性,保留原始交易日;统计展示时按夜盘归集

refactor3.0
Lxy 4 days ago
parent 63192e67d1
commit 8ca645c7bf

@ -101,8 +101,6 @@ async def batch_import_settlement(
total_skipped_all = 0
processed_count = 0
failed_count = 0
# 维护跨文件的去重键集合,用于文件间去重
batch_existing_keys = set()
for file in files:
if not file.filename.endswith(('.xls', '.xlsx')):
@ -127,12 +125,7 @@ async def batch_import_settlement(
failed_count += 1
continue
# 传入已处理文件的去重键,实现文件间去重
result = save_to_db(db, df_futures, df_options, file.filename, batch_existing_keys)
# 将本次导入的新记录去重键加入集合,供后续文件去重使用
if 'new_keys' in result:
batch_existing_keys.update(result['new_keys'])
result = save_to_db(db, df_futures, df_options, file.filename)
file_new = result['futures_count'] + result['options_count']
file_skipped = result['futures_skipped'] + result['options_skipped']

@ -270,16 +270,14 @@ def save_to_db(
df_futures: pd.DataFrame,
df_options: pd.DataFrame,
filename: str,
batch_existing_keys: set = None,
) -> dict:
"""
将解析后的交易记录保存到数据库含逐条去重校验
去重规则同一交易日 + 同一品种 + 同一交易时间 + 同一价格 => 视为重复跳过
:param batch_existing_keys: 批量导入时已处理文件的去重键集合用于文件间去重
仅与数据库已有记录去重不对本次导入的数据去重允许一次委托分多次成交
:return: {
"batch_id": str, "futures_count": int, "options_count": int, "trade_dates": str,
"futures_skipped": int, "options_skipped": int, "total_parsed": int,
"new_keys": set # 本次导入新增的去重键(用于批量导入时传递给下一个文件)
"futures_skipped": int, "options_skipped": int, "total_parsed": int
}
"""
_load_variety_name_map()
@ -310,14 +308,10 @@ def save_to_db(
if trade_date:
all_dates.add(trade_date)
# 批量加载已有记录的去重键
# 批量加载已有记录的去重键(仅与数据库已有记录去重)
existing_keys = _load_existing_dedup_keys(db, all_dates)
# 合并批量导入时已处理文件的去重键
if batch_existing_keys:
existing_keys = existing_keys | batch_existing_keys
records = []
new_keys_in_batch = set() # 本次导入新增的去重键
skipped_details = [] # 记录被跳过的重复记录详情
# 处理期货记录
@ -330,8 +324,8 @@ def save_to_db(
variety = extract_variety(contract)
raw_date = _normalize_date(row.get('实际成交日期', ''))
trade_time = _normalize_time(row.get('成交时间', ''))
# 根据交易时间重新计算交易日(夜盘归属下一天)
trade_date = recalculate_trade_date(raw_date, trade_time)
# 导入时保留结算单原始交易日,不再根据夜盘时间重新计算
trade_date = raw_date
bs_flag = str(row.get('买/卖', '')).strip()
oc_flag = str(row.get('开/平', '')).strip()
price = safe_float(row.get('成交价'))
@ -352,7 +346,6 @@ def save_to_db(
if futures_parsed <= 3:
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(
trade_type='期货',
symbol=contract,
@ -383,8 +376,8 @@ def save_to_db(
variety = extract_variety(contract)
raw_date = _normalize_date(row.get('成交日期', ''))
trade_time = _normalize_time(row.get('成交时间', ''))
# 根据交易时间重新计算交易日(夜盘归属下一天)
trade_date = recalculate_trade_date(raw_date, trade_time)
# 导入时保留结算单原始交易日,不再根据夜盘时间重新计算
trade_date = raw_date
bs_flag = str(row.get('买/卖', '')).strip()
price = safe_float(row.get('权利金单价'))
@ -402,7 +395,6 @@ def save_to_db(
# 期权盈亏来自权利金(带正负号:卖为正,买为负)
premium = safe_float(row.get('权利金'))
new_keys_in_batch.add(dedup_key)
rec = TradeRecord(
trade_type='期权',
symbol=contract,
@ -463,7 +455,6 @@ def save_to_db(
"futures_parsed": futures_parsed,
"options_parsed": options_parsed,
"skipped_details": skipped_details, # 被跳过的重复记录详情
"new_keys": new_keys_in_batch,
}
@ -492,9 +483,12 @@ def calc_daily_summary(db: DBSession, start_date: str = None, end_date: str = No
})
for r in records:
d = r.trade_date or '未知'
day = daily[d]
day["trade_date"] = d
# 统计展示时21:00 之后的交易归集到下一交易日,但保留原始 trade_date 展示
display_date = r.trade_date or '未知'
stat_date = recalculate_trade_date(display_date, r.trade_time or '')
day = daily[stat_date]
day["trade_date"] = stat_date
day["display_date"] = display_date
day["total_trades"] += 1
pnl = (r.close_pnl or 0) - (r.commission or 0)
day["total_pnl"] += pnl
@ -516,7 +510,7 @@ def calc_daily_summary(db: DBSession, start_date: str = None, end_date: str = No
day = daily[d]
total = day["win_count"] + day["loss_count"]
result.append({
"trade_date": day["trade_date"],
"trade_date": day["display_date"],
"total_trades": day["total_trades"],
"total_pnl": round(day["total_pnl"], 2),
"total_commission": round(day["total_commission"], 2),

Binary file not shown.

Binary file not shown.

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-12

@ -0,0 +1,30 @@
## Context
当前 `trade_parser.py``save_to_db` 中对本次导入的数据做了文件内和跨文件去重,且会根据交易时间重新计算 `trade_date`21:00 之后归属下一交易日)。
业务要求:
- 同一委托拆单成交的数据应全部导入,不应去重。
- 导入时保留结算单原始日期。
- 统计分析时按夜盘规则重新归集交易日,但展示日期仍用原始日期。
## Goals / Non-Goals
**Goals:**
1. 移除导入时对本次导入数据的去重逻辑。
2. 导入时直接使用结算单原始日期作为 `trade_date`
3. 统计分析查询时,新增一个统计用交易日字段,将 21:00 之后的数据归集到下一交易日。
4. 前端统计表格的日期列仍显示原始成交日期。
**Non-Goals:**
- 不修改数据库 schema。
- 不修改单条导入接口路径和返回结构。
## Decisions
1. **去重策略**:仅保留与数据库已有记录的去重,移除 `batch_existing_keys``new_keys_in_batch` 逻辑。
2. **交易日计算**:导入时不再调用 `recalculate_trade_date`;在 `trade_review_service.py` 查询统计时,使用 `recalculate_trade_date(trade_date, trade_time)` 生成统计用交易日,但原始 `trade_date` 字段保留用于展示。
3. **前端展示**:表格中继续使用原始 `trade_date`,统计分组使用后端返回的统计用交易日字段(或在后端完成分组后直接返回)。
## Risks / Trade-offs
- 如果用户重复导入同一文件,会产生重复数据。需要通过业务提示引导用户,本次变更不再自动去重。

@ -0,0 +1,23 @@
## Why
交易复盘导入数据时,存在同一品种同一价格同一时间多次成交(拆单成交),不应被判定为重复数据而跳过。同时,导入时应保留结算单原始交易日,仅在统计分析展示时按夜盘规则重新归集交易日,且前端日期显示保持原始日期不变。
## What Changes
1. **移除所有文件内/跨文件去重**:导入时不再对本次导入的数据做任何重复性校验,只保留数据库已有记录的去重。
2. **保留原始交易日**:导入时直接使用结算单中的实际成交日期作为 `trade_date`,不再根据 21:00 之后的时间修改交易日。
3. **展示时归集交易日**:统计分析查询时,将 21:00 之后的交易归集到下一个交易日进行统计,但前端表格中日列仍显示原始成交日期。
## Capabilities
### New Capabilities
- 无
### Modified Capabilities
- 无
## Impact
- `app/services/trade_parser.py`:移除 `batch_existing_keys`、`new_keys_in_batch` 等去重逻辑;移除 `recalculate_trade_date` 调用。
- `app/api/trade_review.py`:批量导入时不再维护跨文件去重键。
- `app/services/trade_review_service.py`:查询统计时按夜盘规则重新计算统计用交易日。

@ -0,0 +1,18 @@
## 1. 移除导入时的本次数据去重
- [x] 1.1 修改 `app/services/trade_parser.py`:移除 `batch_existing_keys` 参数、`new_keys_in_batch` 集合及所有向该集合添加去重键的逻辑
- [x] 1.2 修改 `app/api/trade_review.py`:批量导入时不再创建和传递 `batch_existing_keys`,处理每个文件时独立调用 `save_to_db`
## 2. 导入时使用原始交易日
- [x] 2.1 修改 `app/services/trade_parser.py`:导入期货和期权记录时,直接使用 `_normalize_date` 后的原始日期作为 `trade_date`,不再调用 `recalculate_trade_date`
## 3. 统计分析展示时按夜盘归集交易日
- [x] 3.1 修改 `app/services/trade_parser.py`:在 `calc_daily_summary` 按交易日聚合统计时,使用 `recalculate_trade_date(trade_date, trade_time)` 生成统计用交易日,但保留原始 `trade_date` 用于前端展示
- [x] 3.2 前端无需修改:表格日期列继续使用后端返回的 `trade_date`(即原始日期)
## 4. 验证与归档
- [ ] 4.1 运行健康检查与相关测试
- [ ] 4.2 提交代码并归档 change
Loading…
Cancel
Save