Files
zszq-trs/项目文档/数据库/排查SQL/互换分红损益_数据导出与验证.sql
T
hjhan 029475ce8d test(swap): 新增互换分红重复计算录制测试+排查SQL(TDD红灯)
分红被重复计入 RealizedPnl 两次的根因:互换/平仓事件的 MarkClosePnl
已含分红成分(DividendIn),导致分红既进 RealizedMtmPnL(盯市列) 又进
RealizedDividend(分红列)。本次提供数据证据与回归基线:

- SwapDividendGoldenRecordTest: 连库录制1875(纯分红型)/1891(混合型)
  5表快照→JSON golden,录制时逐事件拆解 MarkClosePnl=价差+费+分红,
  按 PositionId 汇总出重复计入的分红金额。Step2 离线校验已通过。
  诊断结论:1875 重复302400,1891 重复-18.66,均与 eod 分红列吻合。
- golden JSON 纳入源码树(全量复制规则),修复后可作回归基线。
- 排查SQL工具箱: 基于真实DDL核对全部列名(swap_flow_event.EventDate/
  bond_payment_info.paying_interest 等),含定位样本/导出/验证/守恒4步。
- appsettings.json 测试库 zszq→glms 前缀修正。

详见 项目文档/互换分红损益字段语义与重复计算分析.md(9e6af84e)。
2026-06-26 09:13:37 +08:00

304 lines
16 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- ============================================================================
-- 互换分红损益:数据导出与重复计算验证 SQL 工具箱
-- ----------------------------------------------------------------------------
-- ⚠ 本脚本所有列名均取自真实库 DDLglms_yltrs_ylcms.sql),非实体类属性名。
-- 实体类属性名与库列名存在差异(如 swap_flow_event.EventDate、
-- bond_payment_info.paying_interest、swap_position.IsInitial 等)。
--
-- 目的:
-- 1) 定位并导出一笔"带分红的互换交易"完整数据,作 golden source 种子;
-- 2) 用真实库数据验证"分红被重复计算 2 次"
-- 3) 生命周期守恒校验。
--
-- 涉及表与关键列(取自 DDL,列名以反引号为准):
-- swap_event 事件主表(窄列)id,SwapTradeId,ValueDate,EventType,
-- EventReason,EventData(json),Invalid,BackId,OptId,OptTime,ClientCashId
-- ※ 无 SwapTradeNo/UnderlyingCode/金额,这些在 EventData 或流水表
-- swap_position 持仓:PositionId,SwapTradeId,UnderlyingCode,PosiQuantity,
-- PosiNotionalValue,PosiNetPrice,PosiGrossPrice,IsInitial(0实时/1期初),
-- PosiDividendIncome,PosiTradingFeePending,Invalid
-- swap_flow_event 流水(分红核心)EventDate(非ValueDate!),SwapTradeId,SwapTradeNo,
-- EventType,PositionId,Quantity,MarkClosePnl,DividendIn,DividendPending,
-- CloseFee,DataState(0废弃/1等待/100完成)
-- eod_swap_position 日终归档:ValueDate,SwapTradeId,PositionId,TdCloseMtmPnl,TdCloseDividend,
-- RealizedMtmPnL,RealizedDividend,RealizedFee,RealizedInterest,
-- RealizedInterestFee,RealizedPnl,DV01(大写),PosiDividendSum,Invalid
-- eod_swap 日终汇总:ValueDate,SwapTradeId,SwapTradeNo,TdRealizedPnL,RealizedPnL,
-- PostionValue
-- bond_payment_info 债券付息:underlying_code,pay_date_PL,paying_interest(非payment_interest!),
-- paying_principal,paying_price
--
-- EventType 枚举(注意 swap_event 与 swap_flow_event 取值不同!):
-- swap_event.EventType (SwapEventTypeEnum): 展期1/平仓2/互换3/自动互换4/回退5/合成持仓6...
-- swap_flow_event.EventType (SwapFlowEventTypeEnum): 开仓1/平仓2/互换3/自动互换4
-- → 分红型互换在两表均为 EventType IN (3,4)
-- ============================================================================
-- ============================================================================
-- 第 0 步:定位一笔"带分红的互换交易"作为样本
-- ============================================================================
-- 思路:互换/自动互换事件(EventType in 3,4) 的 DividendIn != 0,即发生过分红型互换。
-- swap_flow_event 自带 SwapTradeNo(人类可读),无需关联 swap_event。
-- DataState=100 仅取已完成流水。
SELECT SwapTradeId,
MAX(SwapTradeNo) AS SwapTradeNo,
COUNT(*) AS 互换事件数,
SUM(DividendIn) AS 互换事件分红合计,
SUM(MarkClosePnl) AS 互换事件盯市合计,
MAX(EventDate) AS 最近事件日
FROM swap_flow_event
WHERE EventType IN (3, 4) -- 互换 / 自动互换
AND DividendIn <> 0 -- 真正发生过分红
AND DataState = 100 -- 仅完成的
GROUP BY SwapTradeId
ORDER BY SUM(DividendIn) DESC, MAX(EventDate) DESC
LIMIT 20;
-- 选定其中一行 SwapTradeId,填入下面 @TargetTradeId。
-- ============================================================================
-- 第 1 步:单笔交易完整数据导出(golden source 种子)
-- ============================================================================
-- 用法:把 @TargetTradeId 改为第 0 步选出的值,逐段执行。
SET @TargetTradeId := 1874; -- ← 替换为实际样本 SwapTradeId
-- 1.1 互换事件主表(窄列;EventData 是 json,含详细快照)
SELECT id, SwapTradeId, ValueDate, EventType, EventReason,
Invalid, BackId, OptId, OptName, OptTime, ClientCashId
FROM swap_event
WHERE SwapTradeId = @TargetTradeId
ORDER BY ValueDate, id;
-- 1.2 互换持仓(区分期初/实时:IsInitial 1=期初, 0=实时)
SELECT PositionId, SwapTradeId, UnderlyingCode, PosiDirection, PositionType,
PosiQuantity, PosiNotionalValue, PosiNetPrice, PosiGrossPrice,
ContractSize, CountRatio, PosiTradingFee, PosiTradingFeePending,
PosiDividendIncome, IsInitial, Invalid
FROM swap_position
WHERE SwapTradeId = @TargetTradeId
ORDER BY IsInitial DESC, PositionId;
-- 说明:IsInitial=1 是期初开仓腿;=0 是实时持仓(会随平仓/互换变动)。
-- 1.3 流水事件(分红核心表,导出全字段便于复盘)
SELECT id, EventDate, SwapTradeId, SwapTradeNo, EventType, EventReason,
PositionId, PayDirection, PositionType, UnderlyingCode, Quantity,
TradingAmount, TradingAmountAvg, TradingAmountFeeAvg,
TradingFee, TradingFeePending, DividendPending,
MarkClosePnl, DividendIn, CloseFee, DataState, ClientCashId
FROM swap_flow_event
WHERE SwapTradeId = @TargetTradeId
ORDER BY EventDate, EventType, id;
-- 1.4 日终持仓归档(按日快照,含所有 TdClose* / Realized* 字段)
SELECT id, ValueDate, SwapTradeId, PositionId, PosiDirection, PositionType,
UnderlyingCode, PosiQuantity, PosiNotionalValue,
PosiNetPrice, PosiGrossPrice, UnderlyingPrice, UnderlyingMarketValue,
TdPosiDividend, PosiMtmPnL, PosiDividendSum, PosiFeePending, PosiProfitSum,
TdCloseQty, TdCloseMtmPnl, TdCloseDividend, TdCloseFee,
RealizedMtmPnL, RealizedDividend, RealizedFee, RealizedInterest, RealizedInterestFee,
RealizedPnl, DV01, PosiStatus, Invalid
FROM eod_swap_position
WHERE SwapTradeId = @TargetTradeId
ORDER BY PositionId, ValueDate;
-- 1.5 日终互换层汇总
SELECT id, ValueDate, SwapTradeId, SwapTradeNo, TdCloseQty,
TdRealizedPnL, RealizedPnL, PostionValue
FROM eod_swap
WHERE SwapTradeId = @TargetTradeId
ORDER BY ValueDate;
-- 1.6 债券付息明细(理论应付分红来源)
-- 先从 swap_position 取该交易挂钩的标的代码:
SELECT DISTINCT UnderlyingCode
FROM swap_position
WHERE SwapTradeId = @TargetTradeId
AND UnderlyingCode IS NOT NULL;
-- 再用取到的 UnderlyingCode 查付息明细(替换 @BondCode):
SET @BondCode := 'PUT_UNDERLYING_CODE_HERE';
SELECT id, underlying_code, inner_code,
pay_date_PL, pay_date_act, paying_interest, paying_principal, paying_price,
interest_tax_rate, event_type, info_source, insert_time
FROM bond_payment_info
WHERE underlying_code = @BondCode
ORDER BY pay_date_PL;
-- ============================================================================
-- 第 2 步:分红重复计算验证(证明"分红被算 2 次"
-- ============================================================================
-- 根因链路(SwapEodPositionService.cs):
-- SetPriceInfoByFlowEvent:1610 TdCloseMtmPnl = Σ unwindEvents.MarkClosePnl
-- (互换/平仓事件的 MarkClosePnl 已含分红)
-- UpdateEodPosition:1486 RealizedMtmPnL += TdCloseMtmPnl ← 分红第1次进"盯市"列
-- UpdateEodPosition:1488/1607 TdCloseDividend = Σ DividendIn
-- UpdateEodPosition:1494 RealizedDividend += TdCloseDividend ← 分红第2次进"分红"列
-- SaveEodSwap:1869 eod_swap.RealizedPnL = Σ(RealizedMtmPnL + RealizedDividend + ...)
-- → 分红在盯市列和分红列各计一次 = 2 次
--
-- 验证思路:若 MarkClosePnl 含分红,则同一事件日同一持仓满足:
-- 该日"盯市列中扣除纯平仓价差后的余额" ≈ "分红列",且两者都进了 RealizedPnL → 重复。
SET @TargetTradeId := 1874; -- ← 替换为实际样本
-- 2.1 逐日核对:盯市列 vs 分红列
-- 关键比对:盯市列里扣除"纯平仓事件(EventType=2)的价差"后,剩余是否≈分红列。
-- 若是,说明互换/自动互换事件(EventType in 3,4)的 MarkClosePnl 含分红。
SELECT esp.ValueDate,
esp.PositionId,
esp.TdCloseMtmPnl AS 当日盯市列,
esp.TdCloseDividend AS 当日分红列,
-- 当日纯平仓事件(EventType=2)的盯市价差合计(理论上=纯价差,不含分红)
(SELECT COALESCE(SUM(sfe2.MarkClosePnl), 0)
FROM swap_flow_event sfe2
WHERE sfe2.SwapTradeId = esp.SwapTradeId
AND sfe2.PositionId = esp.PositionId
AND sfe2.EventType = 2
AND sfe2.DataState = 100
AND sfe2.EventDate = esp.ValueDate) AS 纯平仓盯市价差,
-- 盯市列 - 纯平仓价差 = 互换/自动互换事件贡献的盯市成分(若≈分红列→含分红)
(esp.TdCloseMtmPnl - (
SELECT COALESCE(SUM(sfe2.MarkClosePnl), 0)
FROM swap_flow_event sfe2
WHERE sfe2.SwapTradeId = esp.SwapTradeId
AND sfe2.PositionId = esp.PositionId
AND sfe2.EventType = 2
AND sfe2.DataState = 100
AND sfe2.EventDate = esp.ValueDate
)) AS 盯市列扣除纯平仓后余额,
esp.TdCloseDividend AS 分红列,
esp.RealizedMtmPnL AS 累计盯市已实现,
esp.RealizedDividend AS 累计分红已实现,
esp.RealizedPnl AS 持仓层累计已实现
FROM eod_swap_position esp
WHERE esp.SwapTradeId = @TargetTradeId
AND (esp.TdCloseDividend <> 0 OR esp.TdCloseMtmPnl <> 0)
ORDER BY esp.PositionId, esp.ValueDate;
-- 2.2 全生命周期汇总:盯市列累计 + 分红列累计 vs RealizedPnL
-- 若 MarkClosePnl 含分红:盯市累计里多算了一份分红,导致
-- RealizedMtmPnL + RealizedDividend > 真实盯市价差 + 分红 (多出 ≈ 分红金额)
SELECT esp.PositionId,
MAX(esp.RealizedMtmPnL) AS 最终累计盯市已实现,
MAX(esp.RealizedDividend) AS 最终累计分红已实现,
MAX(esp.RealizedPnl) AS 最终持仓层累计已实现,
-- 互换层汇总公式(SaveEodSwap:1869)的口径:
(MAX(esp.RealizedMtmPnL) + MAX(esp.RealizedDividend)
+ COALESCE(MAX(esp.RealizedFee),0)
+ COALESCE(MAX(esp.RealizedInterest),0)
+ COALESCE(MAX(esp.RealizedInterestFee),0))
AS 按互换层公式重算,
-- 理论上不含费的纯盯市价差(用纯平仓 EventType=2 的 MarkClosePnl 估算):
(SELECT COALESCE(SUM(sfe.MarkClosePnl), 0)
FROM swap_flow_event sfe
WHERE sfe.SwapTradeId = esp.SwapTradeId
AND sfe.PositionId = esp.PositionId
AND sfe.EventType = 2
AND sfe.DataState = 100) AS 纯平仓盯市价差合计
FROM eod_swap_position esp
WHERE esp.SwapTradeId = @TargetTradeId
GROUP BY esp.PositionId;
-- 2.3 一句话诊断:互换/自动互换分红事件的 MarkClosePnl 是否含分红
-- 对每个 EventType in (3,4) 且 DividendIn<>0 的事件日,
-- 检查当日 eod 盯市列是否也包含了等额成分。
SELECT sfe.EventDate,
sfe.PositionId,
sfe.EventType,
sfe.DividendIn AS 事件分红流入,
sfe.MarkClosePnl AS 事件盯市含费,
esp.TdCloseMtmPnl AS 当日盯市列,
esp.TdCloseDividend AS 当日分红列,
CASE
-- 同一互换事件自身:若 MarkClosePnl≈DividendIn,则该事件盯市就含分红
WHEN ABS(sfe.MarkClosePnl - sfe.DividendIn) < 0.01 AND sfe.DividendIn <> 0
THEN '⚠该事件MarkClosePnl≈DividendIn→盯市含分红(根因)'
-- 当日整列:盯市列≈分红列
WHEN esp.TdCloseMtmPnl <> 0
AND ABS(esp.TdCloseMtmPnl - esp.TdCloseDividend) < 0.01
THEN '⚠当日盯市列≈分红列→重复'
ELSE '需人工核对'
END AS 诊断
FROM swap_flow_event sfe
JOIN eod_swap_position esp
ON esp.SwapTradeId = sfe.SwapTradeId
AND esp.PositionId = sfe.PositionId
AND esp.ValueDate = sfe.EventDate
WHERE sfe.SwapTradeId = @TargetTradeId
AND sfe.EventType IN (3, 4)
AND sfe.DividendIn <> 0
AND sfe.DataState = 100
ORDER BY sfe.EventDate;
-- ============================================================================
-- 第 3 步:生命周期守恒校验(累计已实现分红 vs 理论应付分红)
-- ============================================================================
-- 含义:一笔互换交易从开仓到全部平仓,"已实现分红收益总额"应等于持仓期间
-- 该债券应付分红(税后)的累加。任何偏差说明核算有误。
-- 若存在第 2 步的重复计算,累计已实现分红会被放大,本步量化偏差作修复后回归基线。
SET @TargetTradeId := 1874;
-- 3.1 实际已实现分红(从事件流水 DividendIn 累加)
SELECT
SUM(CASE WHEN EventType IN (2,3,4) THEN DividendIn ELSE 0 END) AS 事件流水已实现分红合计,
SUM(CASE WHEN EventType IN (3,4) THEN DividendIn ELSE 0 END) AS 其中互换事件分红,
SUM(CASE WHEN EventType = 2 THEN DividendIn ELSE 0 END) AS 其中平仓事件分红
FROM swap_flow_event
WHERE SwapTradeId = @TargetTradeId
AND DataState = 100;
-- 3.2 日终表口径的最终累计已实现分红(应与 3.1 一致)
SELECT PositionId,
MAX(RealizedDividend) AS 日终表累计已实现分红,
MAX(RealizedPnl) AS 日终表累计已实现盈亏
FROM eod_swap_position
WHERE SwapTradeId = @TargetTradeId
GROUP BY PositionId;
-- 3.3 理论应付分红(税后)—— 需人工带入持仓区间与标的
-- 业务口径(BondPaymentService.CalcPayment):
-- totalPayment = CalcPayment(UnderlyingCode, StartDate, EndDate, Qty, shortRatio, dirRatio)
-- 理论税后分红 = totalPayment / (1 + tax) * (1 - tax)
-- 这里给出从 bond_payment_info 直接估算的简化版(仅供量级对照):
SET @BondCode := 'PUT_UNDERLYING_CODE_HERE';
SET @StartDate := '2024-01-01';
SET @EndDate := '2024-12-31';
SELECT underlying_code,
SUM(COALESCE(paying_interest, 0)) * 0.01 AS 区间每张利息合计_相对值,
COUNT(*) AS 付息次数
FROM bond_payment_info
WHERE underlying_code = @BondCode
AND pay_date_PL BETWEEN @StartDate AND @EndDate
GROUP BY underlying_code;
-- 说明:paying_interest 为"每张兑付利息额",×0.01 转相对价后还需 ×持仓数量 ×方向,
-- 再做税后调整,才能与 3.1/3.2 对齐。精确口径见 BondPaymentService.CalcPayment。
-- ============================================================================
-- 附录:导出为 json / csv 的方式
-- ============================================================================
-- 【方式A:MySQL 客户端导出(推荐,最简单)】
-- 在 Navicat / DBeaver / MySQL Workbench 中执行上述任一 SELECT,结果区右键
-- "导出" → 选 JSON / CSV / Excel。推荐把 1.1~1.6 各导一份,按表名命名:
-- swap_event.json / swap_position.json / swap_flow_event.json /
-- eod_swap_position.json / eod_swap.json / bond_payment_info.json
--
-- 【方式B:命令行 mysqldump(整表+DDL,含 CREATE)】
-- mysqldump -h<host> -u<user> -p<db> swap_flow_event \
-- --where="SwapTradeId=1874 AND DataState=100" \
-- --skip-add-drop-table --no-create-info > sfe_1874.sql
--
-- 【方式CSELECT ... INTO OUTFILE(服务端导 csv,需 FILE 权限)】
-- SELECT ... FROM swap_flow_event WHERE SwapTradeId=1874
-- INTO OUTFILE '/tmp/sfe_1874.csv'
-- FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\r\n';
--
-- golden source 种子建议:用方式A导出 1.1~1.6 共 6 个 json
-- 连同 trade 主记录,作为"一笔带分红互换交易"的完整快照纳入版本库。