|
|
|
@ -5,6 +5,7 @@
|
|
|
|
import re
|
|
|
|
import re
|
|
|
|
import uuid
|
|
|
|
import uuid
|
|
|
|
import json
|
|
|
|
import json
|
|
|
|
|
|
|
|
import logging
|
|
|
|
from datetime import datetime
|
|
|
|
from datetime import datetime
|
|
|
|
from collections import defaultdict
|
|
|
|
from collections import defaultdict
|
|
|
|
from pathlib import Path
|
|
|
|
from pathlib import Path
|
|
|
|
@ -15,6 +16,8 @@ from sqlalchemy.orm import Session as DBSession
|
|
|
|
|
|
|
|
|
|
|
|
from app.analysis_models import TradeRecord, TradeImportBatch
|
|
|
|
from app.analysis_models import TradeRecord, TradeImportBatch
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
# 品种代码 -> 中文名 映射(反向映射:从合约代码前缀查中文名)
|
|
|
|
# 品种代码 -> 中文名 映射(反向映射:从合约代码前缀查中文名)
|
|
|
|
_VARIETY_NAME_MAP = {}
|
|
|
|
_VARIETY_NAME_MAP = {}
|
|
|
|
|
|
|
|
|
|
|
|
@ -193,6 +196,35 @@ def _normalize_time(time_val) -> str:
|
|
|
|
return s
|
|
|
|
return s
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _load_existing_dedup_keys(db: DBSession, trade_dates: set) -> set:
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
根据涉及的交易日,批量加载已有记录的去重键集合
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
if not trade_dates:
|
|
|
|
|
|
|
|
logger.warning("[去重校验] trade_dates 为空,跳过查询")
|
|
|
|
|
|
|
|
return set()
|
|
|
|
|
|
|
|
logger.info(f"[去重校验] 查询已有记录,日期范围: {sorted(trade_dates)}")
|
|
|
|
|
|
|
|
existing = db.query(
|
|
|
|
|
|
|
|
TradeRecord.trade_date,
|
|
|
|
|
|
|
|
TradeRecord.variety,
|
|
|
|
|
|
|
|
TradeRecord.trade_time,
|
|
|
|
|
|
|
|
TradeRecord.price,
|
|
|
|
|
|
|
|
).filter(
|
|
|
|
|
|
|
|
TradeRecord.trade_date.in_(trade_dates)
|
|
|
|
|
|
|
|
).all()
|
|
|
|
|
|
|
|
logger.info(f"[去重校验] 从数据库加载到 {len(existing)} 条已有记录")
|
|
|
|
|
|
|
|
keys = {_build_dedup_key(r[0], r[1], r[2] or '', r[3] or 0.0) for r in existing}
|
|
|
|
|
|
|
|
if keys:
|
|
|
|
|
|
|
|
sample = list(keys)[:3]
|
|
|
|
|
|
|
|
logger.info(f"[去重校验] 去重键示例: {sample}")
|
|
|
|
|
|
|
|
return keys
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def save_to_db(
|
|
|
|
def save_to_db(
|
|
|
|
db: DBSession,
|
|
|
|
db: DBSession,
|
|
|
|
df_futures: pd.DataFrame,
|
|
|
|
df_futures: pd.DataFrame,
|
|
|
|
@ -200,16 +232,46 @@ def save_to_db(
|
|
|
|
filename: str,
|
|
|
|
filename: str,
|
|
|
|
) -> dict:
|
|
|
|
) -> dict:
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
将解析后的交易记录保存到数据库
|
|
|
|
将解析后的交易记录保存到数据库(含逐条去重校验)
|
|
|
|
:return: {"batch_id": str, "futures_count": int, "options_count": int, "trade_dates": str}
|
|
|
|
去重规则:同一交易日 + 同一品种 + 同一交易时间 + 同一价格 => 视为重复,跳过
|
|
|
|
|
|
|
|
:return: {
|
|
|
|
|
|
|
|
"batch_id": str, "futures_count": int, "options_count": int, "trade_dates": str,
|
|
|
|
|
|
|
|
"futures_skipped": int, "options_skipped": int, "total_parsed": int
|
|
|
|
|
|
|
|
}
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
_load_variety_name_map()
|
|
|
|
_load_variety_name_map()
|
|
|
|
batch_id = str(uuid.uuid4())
|
|
|
|
batch_id = str(uuid.uuid4())
|
|
|
|
all_dates = set()
|
|
|
|
all_dates = set()
|
|
|
|
futures_count = 0
|
|
|
|
futures_count = 0
|
|
|
|
options_count = 0
|
|
|
|
options_count = 0
|
|
|
|
|
|
|
|
futures_skipped = 0
|
|
|
|
|
|
|
|
options_skipped = 0
|
|
|
|
|
|
|
|
futures_parsed = 0
|
|
|
|
|
|
|
|
options_parsed = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 第一遍:收集所有涉及的交易日,用于批量查询已有记录
|
|
|
|
|
|
|
|
if not df_futures.empty:
|
|
|
|
|
|
|
|
for _, row in df_futures.iterrows():
|
|
|
|
|
|
|
|
contract = str(row.get('合约', '')).strip()
|
|
|
|
|
|
|
|
if not contract or contract == '合计':
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
trade_date = _normalize_date(row.get('实际成交日期', ''))
|
|
|
|
|
|
|
|
if trade_date:
|
|
|
|
|
|
|
|
all_dates.add(trade_date)
|
|
|
|
|
|
|
|
if not df_options.empty:
|
|
|
|
|
|
|
|
for _, row in df_options.iterrows():
|
|
|
|
|
|
|
|
contract = str(row.get('品种合约', '')).strip()
|
|
|
|
|
|
|
|
if not contract or contract == '合计':
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
trade_date = _normalize_date(row.get('成交日期', ''))
|
|
|
|
|
|
|
|
if trade_date:
|
|
|
|
|
|
|
|
all_dates.add(trade_date)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 批量加载已有记录的去重键
|
|
|
|
|
|
|
|
existing_keys = _load_existing_dedup_keys(db, all_dates)
|
|
|
|
|
|
|
|
|
|
|
|
records = []
|
|
|
|
records = []
|
|
|
|
|
|
|
|
new_keys_in_batch = set() # 防止本次导入文件内部重复
|
|
|
|
|
|
|
|
|
|
|
|
# 处理期货记录
|
|
|
|
# 处理期货记录
|
|
|
|
if not df_futures.empty:
|
|
|
|
if not df_futures.empty:
|
|
|
|
@ -217,12 +279,24 @@ def save_to_db(
|
|
|
|
contract = str(row.get('合约', '')).strip()
|
|
|
|
contract = str(row.get('合约', '')).strip()
|
|
|
|
if not contract or contract == '合计':
|
|
|
|
if not contract or contract == '合计':
|
|
|
|
continue
|
|
|
|
continue
|
|
|
|
|
|
|
|
futures_parsed += 1
|
|
|
|
variety = extract_variety(contract)
|
|
|
|
variety = extract_variety(contract)
|
|
|
|
trade_date = _normalize_date(row.get('实际成交日期', ''))
|
|
|
|
trade_date = _normalize_date(row.get('实际成交日期', ''))
|
|
|
|
trade_time = _normalize_time(row.get('成交时间', ''))
|
|
|
|
trade_time = _normalize_time(row.get('成交时间', ''))
|
|
|
|
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('成交价'))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
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}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
new_keys_in_batch.add(dedup_key)
|
|
|
|
rec = TradeRecord(
|
|
|
|
rec = TradeRecord(
|
|
|
|
trade_type='期货',
|
|
|
|
trade_type='期货',
|
|
|
|
symbol=contract,
|
|
|
|
symbol=contract,
|
|
|
|
@ -230,7 +304,7 @@ def save_to_db(
|
|
|
|
symbol_name=_VARIETY_NAME_MAP.get(variety, ''),
|
|
|
|
symbol_name=_VARIETY_NAME_MAP.get(variety, ''),
|
|
|
|
direction='买' if '买' in bs_flag else '卖',
|
|
|
|
direction='买' if '买' in bs_flag else '卖',
|
|
|
|
offset='开' if '开' in oc_flag else ('平' if '平' in oc_flag else ''),
|
|
|
|
offset='开' if '开' in oc_flag else ('平' if '平' in oc_flag else ''),
|
|
|
|
price=safe_float(row.get('成交价')),
|
|
|
|
price=price,
|
|
|
|
volume=safe_float(row.get('手数')),
|
|
|
|
volume=safe_float(row.get('手数')),
|
|
|
|
amount=safe_float(row.get('成交额')),
|
|
|
|
amount=safe_float(row.get('成交额')),
|
|
|
|
close_pnl=safe_float(row.get('平仓盈亏')),
|
|
|
|
close_pnl=safe_float(row.get('平仓盈亏')),
|
|
|
|
@ -242,8 +316,6 @@ def save_to_db(
|
|
|
|
)
|
|
|
|
)
|
|
|
|
records.append(rec)
|
|
|
|
records.append(rec)
|
|
|
|
futures_count += 1
|
|
|
|
futures_count += 1
|
|
|
|
if trade_date:
|
|
|
|
|
|
|
|
all_dates.add(trade_date)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 处理期权记录
|
|
|
|
# 处理期权记录
|
|
|
|
if not df_options.empty:
|
|
|
|
if not df_options.empty:
|
|
|
|
@ -251,11 +323,19 @@ def save_to_db(
|
|
|
|
contract = str(row.get('品种合约', '')).strip()
|
|
|
|
contract = str(row.get('品种合约', '')).strip()
|
|
|
|
if not contract or contract == '合计':
|
|
|
|
if not contract or contract == '合计':
|
|
|
|
continue
|
|
|
|
continue
|
|
|
|
|
|
|
|
options_parsed += 1
|
|
|
|
variety = extract_variety(contract)
|
|
|
|
variety = extract_variety(contract)
|
|
|
|
trade_date = _normalize_date(row.get('成交日期', ''))
|
|
|
|
trade_date = _normalize_date(row.get('成交日期', ''))
|
|
|
|
trade_time = _normalize_time(row.get('成交时间', ''))
|
|
|
|
trade_time = _normalize_time(row.get('成交时间', ''))
|
|
|
|
bs_flag = str(row.get('买/卖', '')).strip()
|
|
|
|
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
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
new_keys_in_batch.add(dedup_key)
|
|
|
|
rec = TradeRecord(
|
|
|
|
rec = TradeRecord(
|
|
|
|
trade_type='期权',
|
|
|
|
trade_type='期权',
|
|
|
|
symbol=contract,
|
|
|
|
symbol=contract,
|
|
|
|
@ -263,7 +343,7 @@ def save_to_db(
|
|
|
|
symbol_name=_VARIETY_NAME_MAP.get(variety, ''),
|
|
|
|
symbol_name=_VARIETY_NAME_MAP.get(variety, ''),
|
|
|
|
direction='买' if '买' in bs_flag else '卖',
|
|
|
|
direction='买' if '买' in bs_flag else '卖',
|
|
|
|
offset='',
|
|
|
|
offset='',
|
|
|
|
price=safe_float(row.get('权利金单价')),
|
|
|
|
price=price,
|
|
|
|
volume=safe_float(row.get('成交量')),
|
|
|
|
volume=safe_float(row.get('成交量')),
|
|
|
|
amount=safe_float(row.get('权利金')),
|
|
|
|
amount=safe_float(row.get('权利金')),
|
|
|
|
close_pnl=0.0,
|
|
|
|
close_pnl=0.0,
|
|
|
|
@ -275,8 +355,6 @@ def save_to_db(
|
|
|
|
)
|
|
|
|
)
|
|
|
|
records.append(rec)
|
|
|
|
records.append(rec)
|
|
|
|
options_count += 1
|
|
|
|
options_count += 1
|
|
|
|
if trade_date:
|
|
|
|
|
|
|
|
all_dates.add(trade_date)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if records:
|
|
|
|
if records:
|
|
|
|
db.add_all(records)
|
|
|
|
db.add_all(records)
|
|
|
|
@ -301,11 +379,22 @@ def save_to_db(
|
|
|
|
db.add(batch)
|
|
|
|
db.add(batch)
|
|
|
|
db.commit()
|
|
|
|
db.commit()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
|
|
f"[去重校验] 导入完成: 文件={filename}, "
|
|
|
|
|
|
|
|
f"期货 解析={futures_parsed} 新增={futures_count} 跳过={futures_skipped}, "
|
|
|
|
|
|
|
|
f"期权 解析={options_parsed} 新增={options_count} 跳过={options_skipped}, "
|
|
|
|
|
|
|
|
f"已有去重键数量={len(existing_keys)}"
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
"batch_id": batch_id,
|
|
|
|
"batch_id": batch_id,
|
|
|
|
"futures_count": futures_count,
|
|
|
|
"futures_count": futures_count,
|
|
|
|
"options_count": options_count,
|
|
|
|
"options_count": options_count,
|
|
|
|
"trade_dates": trade_dates_str,
|
|
|
|
"trade_dates": trade_dates_str,
|
|
|
|
|
|
|
|
"futures_skipped": futures_skipped,
|
|
|
|
|
|
|
|
"options_skipped": options_skipped,
|
|
|
|
|
|
|
|
"futures_parsed": futures_parsed,
|
|
|
|
|
|
|
|
"options_parsed": options_parsed,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|