refactor(trade): 优化除权信息处理逻辑和数据查询策略

- 在 FindExDividendByBusinessKey 方法中添加注释说明业务唯一键按"标的 + 自然日"定义
- 实现字段独立合并策略,当前值非零时覆盖旧值,为零时保留旧值
- 保存前统一截断时间部分,确保同一天不同时间能命中同一个自然日业务键
- 先在当前批次内按业务键归并,避免重复行生成多条数据库记录
- 除权数据查询时不使用 SQL 左连接,防止重复行扩增影响风险计算
- 使用不区分大小写的 UnderlyingCode 匹配,兼容代码大小写差异
- 除权记录不参与 SQL 左连接,先完成基础关联再内存查找唯一记录
- 风险对象的除权 Pv 重算规则与持仓处理保持一致,仅在股票交易类型下执行
This commit is contained in:
张名锐
2026-08-13 11:09:50 +08:00
parent 5a51d66970
commit 04f4468be5
2 changed files with 35 additions and 5 deletions
@@ -39,6 +39,10 @@ namespace YLErp.Modules.EodModule
predicate = PredicateBuilder.Create<T>(n => n.ValueDate == settleDate).And(predicate);
}
// 除权数据不在这里做 SQL 左连接:同一标的一天只允许一条有效除权记录,
// 但历史脏数据可能存在重复行。左连接会把一条 EOD 持仓扩成多行,进而重复
// 参与后续风险/结算计算。先取得 EOD+BOD 的唯一持仓结果,再按标的代码匹配
// 除权记录,可以把重复业务键暴露为 ToDictionary 异常,而不是静默扩行。
var query = from eod in DbContext.Set<T>().Where(predicate)
join bod in DbContext.BodTradePosition.Where(n => n.ValueDate == bodDate)
on new { eod.BookId, eod.TradeType, eod.PositionType, eod.UnderlyingCode, ExchangeOptionCode = eod.ExchangeOptionCode ?? string.Empty }
@@ -57,6 +61,9 @@ namespace YLErp.Modules.EodModule
var datas = query.ToArray();
var diviService = new TradeModule.DealModule.DividendService(OptUser);
// 除权查询集中复用 DividendService 的有效记录条件。字典使用不区分大小写的
// UnderlyingCode 匹配,兼容 EOD 与除权表代码大小写差异;如果同日同代码仍有
// 多条有效记录,ToDictionary 会失败,提示迁移/结算前先清理重复数据。
var dividendDict = diviService.GetExDividendQuery(settleDate)
.ToDictionary(O => O.UnderlyingCode, O => O, StringComparer.OrdinalIgnoreCase);
var eodPriceProvider = new EodPriceProvider(settleDate);
@@ -64,6 +71,9 @@ namespace YLErp.Modules.EodModule
{
var eod = data.eod;
var bod = data.bod;
// 命中除权数据后仍沿用原有股票结算分支:只重算除权后的收盘价和数量,
// 并保留原 Pv 的正负方向。其他 TradeType 当前不进入该分支,避免扩大
// 本次查询重构的业务范围。
if (dividendDict.TryGetValue(eod.UnderlyingCode, out var dividend))
{
if (data.eod.TradeType == "股票")
@@ -102,6 +112,9 @@ namespace YLErp.Modules.EodModule
predicate = PredicateBuilder.Create<TPos>(n => n.ValueDate == settleDate).And(predicate);
}
// 带风险数据的重载与上面的持仓重载采用相同策略:除权记录不参与 SQL 左连接,
// 先完成 EOD、BOD、Risk 的行级关联,再在内存中按标的代码查找唯一除权记录,
// 防止除权表重复行复制风险记录。
var query = from eod in DbContext.Set<TPos>().AsNoTracking().Where(predicate)
join bod in DbContext.BodTradePosition.Where(n => n.ValueDate == bodDate)
on new { eod.BookId, eod.TradeType, eod.PositionType, eod.UnderlyingCode, ExchangeOptionCode = eod.ExchangeOptionCode ?? string.Empty }
@@ -123,6 +136,8 @@ namespace YLErp.Modules.EodModule
var datas = query.ToArray();
var diviService = new TradeModule.DealModule.DividendService(OptUser);
// 与无风险重载保持同一数据来源、日期条件和大小写无关的代码匹配规则;重复
// 有效记录会在这里显式失败,而不是让一条持仓对应多条风险结果。
var dividendDict = diviService.GetExDividendQuery(settleDate)
.ToDictionary(O => O.UnderlyingCode, O => O, StringComparer.OrdinalIgnoreCase);
var eodPriceProvider = new EodPriceProvider(settleDate);
@@ -130,6 +145,7 @@ namespace YLErp.Modules.EodModule
{
var pos = data.eod;
var bod = data.bod;
// 风险对象的除权 Pv 重算规则与上一个重载保持一致,仅在股票交易类型下执行。
if (dividendDict.TryGetValue(pos.UnderlyingCode, out var dividend))
{
if (data.eod.TradeType == "股票")